🎮

Amazon Q Developer CLIとPygameを使って簡単な横スクロールアクションゲームを作ってみた

に公開

今回はAmazon Q Developer CLIとPygameを使って簡単な横スクロールアクションゲームを作ってみたいと思います。

はじめに

Amazon Q Developerについて

Amazon Q Developerは、生成AIを活用した会話アシスタントです。

特に、Amazon Q Developer CLIはChatGPTのCLI版みたいなものだと思って頂ければイメージしやすいかも知れません。

Amazon Q Developer CLIは今のところ(2025/05時点)無料で利用できます。

Pygameについて

Pygameはその名の通りゲームを製作するために設計されたクロスプラットフォームのPythonモジュールになっています。

公式サイトに作例がありますが、かなり本格的なゲームも作ることができます。

https://www.pygame.org/tags/all

Amazon Q Developer CLIのインストール

OSによってインストール方法が異なりますが、Macの場合はHomebrewで簡単にインストールすることができます。

$ brew install amazon-q

その他のOSについては以下公式ドキュメントをご確認下さい。

https://docs.aws.amazon.com/ja_jp/amazonq/latest/qdeveloper-ug/command-line-installing.html

Pygamesのインストール

pip を利用して、以下でインストール可能です。

$ pip install pygame

作ってみた

表題の通り、Amazon Q Developer CLIに簡単な横スクロールアクションゲームを作ってもらいました。

プレイ映像は以下の通りです。

ランダムに生成される地形を自転車(見た目はほぼトロッコですが...笑)で走りながら、道中のコインを取得しつつ距離を稼ぐゲームです。

上記は私の好きなチャリ走というゲームにインスピレーションを受け作成をしております。

生成AIを利用し極短時間で作成したものになるのでまだまだバグ等もあるかと思いますが、コードは以下の通りです。

Pythonコード
main.py
import pygame
import random

# Pygameの初期化
pygame.init()

# 定数
SCREEN_WIDTH = 800
SCREEN_HEIGHT = 600
WHITE = (255, 255, 255)
BLACK = (0, 0, 0)
RED = (255, 0, 0)
GREEN = (0, 255, 0)
BLUE = (0, 0, 255)
YELLOW = (255, 255, 0)
GRAY = (128, 128, 128)
DARK_GRAY = (64, 64, 64)

class Player:
    def __init__(self, x, y):
        self.x = x
        self.y = y
        self.width = 40
        self.height = 30
        self.vel_y = 0
        self.vel_x = 0
        self.on_ground = False
        self.speed = 5
        self.jump_power = -15
        self.gravity = 0.8
        self.ground_y = SCREEN_HEIGHT - 100
        self.can_jump_timer = 0  # ジャンプ可能時間
        self.was_in_hole = False  # 前フレームで穴にいたか
        self.falling_from_hole = False  # 穴から落下中かどうか
        
    def update(self, terrain):
        # 重力の適用
        self.vel_y += self.gravity
        
        # Y座標の更新
        old_y = self.y
        self.y += self.vel_y
        
        # プレイヤーの足元の複数点で地面をチェック
        left_foot_x = self.x + 5
        right_foot_x = self.x + self.width - 5
        center_foot_x = self.x + self.width // 2
        
        # 複数点での地面の高さを取得
        left_ground = terrain.get_ground_height(left_foot_x)
        right_ground = terrain.get_ground_height(right_foot_x)
        center_ground = terrain.get_ground_height(center_foot_x)
        
        # 地面がある場所の最高点を取得
        ground_heights = [h for h in [left_ground, right_ground, center_ground] if h is not None]
        
        # 現在穴の上にいるかチェック
        currently_in_hole = len(ground_heights) == 0
        
        # 穴から落下状態の管理
        if self.was_in_hole and currently_in_hole and self.vel_y > 0:
            self.falling_from_hole = True
        
        if ground_heights:
            # 地面がある場合
            highest_ground = min(ground_heights)  # Y座標なので小さい値が高い
            
            # 壁衝突判定:穴から落下中に地面に横から衝突した場合のみ
            if (self.falling_from_hole and self.vel_y > 0 and 
                self.y + self.height > highest_ground):
                
                # プレイヤーの中心が地面より下にある場合は壁衝突
                player_center_y = self.y + self.height // 2
                if player_center_y > highest_ground:
                    return True  # ゲームオーバー(壁に衝突)
            
            # 正常な着地判定
            if self.y + self.height >= highest_ground and self.vel_y >= 0:
                self.y = highest_ground - self.height
                self.vel_y = 0
                self.on_ground = True
                self.can_jump_timer = 10  # 10フレーム間はジャンプ可能
                self.falling_from_hole = False  # 着地したので穴からの落下状態をリセット
            else:
                self.on_ground = False
        else:
            # 完全に穴の上にいる場合
            self.on_ground = False
        
        # 穴の状態を更新
        self.was_in_hole = currently_in_hole
        
        # ジャンプ可能時間の減少
        if self.can_jump_timer > 0:
            self.can_jump_timer -= 1
        
        # 画面下に落ちたらゲームオーバー
        if self.y > SCREEN_HEIGHT:
            return True  # ゲームオーバー
        
        return False  # ゲーム続行
    
    def jump(self):
        # 地面にいるか、最近まで地面にいた場合ジャンプ可能
        if self.on_ground or self.can_jump_timer > 0:
            self.vel_y = self.jump_power
            self.on_ground = False
            self.can_jump_timer = 0
            self.falling_from_hole = False  # ジャンプ時は穴からの落下状態をリセット
    
    def draw(self, screen, camera_x):
        draw_x = self.x - camera_x
        # 自転車のシンプルな描画
        # 車体
        pygame.draw.rect(screen, BLUE, (draw_x, self.y, self.width, self.height))
        # タイヤ
        pygame.draw.circle(screen, BLACK, (int(draw_x + 8), int(self.y + self.height)), 8)
        pygame.draw.circle(screen, BLACK, (int(draw_x + self.width - 8), int(self.y + self.height)), 8)

class TerrainPoint:
    def __init__(self, x, y, is_hole=False):
        self.x = x
        self.y = y
        self.is_hole = is_hole

class Terrain:
    def __init__(self):
        self.points = []
        self.base_height = SCREEN_HEIGHT - 100
        self.min_height = 150  # 地面の最高位置(画面上端から150ピクセル下)
        self.max_height = SCREEN_HEIGHT - 50  # 地面の最低位置
        self.segment_width = 40  # 各地形セグメントの幅
        self.generate_initial_terrain()
        
    def generate_initial_terrain(self):
        # 初期地形の生成
        current_x = 0
        current_height = self.base_height
        
        # 最初は平坦な地面から開始
        for i in range(20):
            self.points.append(TerrainPoint(current_x, current_height))
            current_x += self.segment_width
        
        # 地形を先まで生成
        self.extend_terrain(current_x, current_height)
    
    def extend_terrain(self, start_x, start_height):
        current_x = start_x
        current_height = start_height
        
        while current_x < start_x + 2000:  # 2000ピクセル分生成
            # 次の地形タイプを決定
            terrain_type = random.choices(
                ['flat', 'up', 'down', 'hole'], 
                weights=[40, 25, 25, 10]
            )[0]
            
            if terrain_type == 'flat':
                # 平坦
                length = random.randint(3, 8)
                for i in range(length):
                    self.points.append(TerrainPoint(current_x, current_height))
                    current_x += self.segment_width
                    
            elif terrain_type == 'up':
                # 上り坂
                length = random.randint(4, 7)
                max_possible_change = current_height - self.min_height
                height_change = min(random.randint(40, 100), max_possible_change)
                
                if height_change > 0:
                    height_per_step = height_change // length
                    for i in range(length):
                        y = max(self.min_height, current_height - height_per_step * (i + 1))
                        self.points.append(TerrainPoint(current_x, y))
                        current_x += self.segment_width
                    current_height = max(self.min_height, current_height - height_change)
                else:
                    # 上がれない場合は平坦にする
                    for i in range(length):
                        self.points.append(TerrainPoint(current_x, current_height))
                        current_x += self.segment_width
                
            elif terrain_type == 'down':
                # 下り坂
                length = random.randint(4, 7)
                max_possible_change = self.max_height - current_height
                height_change = min(random.randint(40, 100), max_possible_change)
                
                if height_change > 0:
                    height_per_step = height_change // length
                    for i in range(length):
                        y = min(self.max_height, current_height + height_per_step * (i + 1))
                        self.points.append(TerrainPoint(current_x, y))
                        current_x += self.segment_width
                    current_height = min(self.max_height, current_height + height_change)
                else:
                    # 下がれない場合は平坦にする
                    for i in range(length):
                        self.points.append(TerrainPoint(current_x, current_height))
                        current_x += self.segment_width
                        
            elif terrain_type == 'hole':
                # 穴(ジャンプで越えられるサイズに制限)
                # プレイヤーのジャンプ距離を考慮(速度5 × 約30フレーム = 150ピクセル程度)
                hole_width = random.randint(2, 3)  # 2-3セグメント(80-120ピクセル)
                for i in range(hole_width):
                    self.points.append(TerrainPoint(current_x, current_height, is_hole=True))
                    current_x += self.segment_width
    
    def get_ground_height(self, x):
        # 指定されたX座標での地面の高さを取得
        if not self.points:
            return self.base_height
        
        # X座標に対応する地形ポイントを見つける
        for i, point in enumerate(self.points):
            if point.x <= x < point.x + self.segment_width:
                if point.is_hole:
                    return None  # 穴の場合
                else:
                    return point.y
        
        # 見つからない場合は最後のポイントの高さ
        if self.points:
            last_point = self.points[-1]
            return last_point.y if not last_point.is_hole else self.base_height
        
        return self.base_height
    
    def update(self, camera_x):
        # カメラの位置に基づいて新しい地形を生成
        rightmost_x = max(point.x for point in self.points) if self.points else 0
        
        if camera_x + SCREEN_WIDTH > rightmost_x - 1000:
            # 新しい地形を生成
            last_solid_point = None
            for point in reversed(self.points):
                if not point.is_hole:
                    last_solid_point = point
                    break
            
            if last_solid_point:
                self.extend_terrain(rightmost_x, last_solid_point.y)
            else:
                self.extend_terrain(rightmost_x, self.base_height)
    
    def draw(self, screen, camera_x):
        # 地形の描画(線は描画しない)
        visible_points = [p for p in self.points if p.x - camera_x > -100 and p.x - camera_x < SCREEN_WIDTH + 100]
        
        # 地面の塗りつぶしのみ
        for point in visible_points:
            draw_x = point.x - camera_x
            if not point.is_hole:
                pygame.draw.rect(screen, GRAY, 
                               (draw_x, point.y, self.segment_width, SCREEN_HEIGHT - point.y))

class Coin:
    def __init__(self, x, y):
        self.x = x
        self.y = y
        self.radius = 10
        self.collected = False
        self.spawn_x = x  # 生成時のX座標を記録
    
    def draw(self, screen, camera_x):
        if not self.collected:
            draw_x = self.x - camera_x
            # 画面右端から徐々に現れるエフェクト
            if draw_x > SCREEN_WIDTH - 50:
                # 透明度を調整(画面端では薄く)
                alpha = max(0, min(255, (SCREEN_WIDTH - draw_x) * 5))
                if alpha > 50:  # 十分に見える場合のみ描画
                    pygame.draw.circle(screen, YELLOW, (int(draw_x), int(self.y)), self.radius)
            elif -20 <= draw_x <= SCREEN_WIDTH:
                pygame.draw.circle(screen, YELLOW, (int(draw_x), int(self.y)), self.radius)

class Game:
    def __init__(self):
        self.screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))
        pygame.display.set_caption("Bicycle Runner")
        self.clock = pygame.time.Clock()
        self.player = Player(100, SCREEN_HEIGHT - 150)
        self.terrain = Terrain()
        self.coins = []
        self.camera_x = 0
        self.score = 0
        self.distance = 0
        self.game_over = False
        self.font = pygame.font.Font(None, 36)
        self.coin_spawn_timer = 0
        
    def generate_coins(self):
        # コインの動的生成(画面右端の先に配置)
        self.coin_spawn_timer += 1
        if self.coin_spawn_timer > 180:  # 3秒ごと
            # 画面右端より先にコインを配置
            x = self.camera_x + SCREEN_WIDTH + random.randint(100, 300)
            ground_height = self.terrain.get_ground_height(x)
            if ground_height is not None:  # 地面がある場所にのみ配置
                y = ground_height - random.randint(40, 100)
                self.coins.append(Coin(x, y))
            self.coin_spawn_timer = 0
        
        # 古いコインを削除(プレイヤーから遠く離れたもの)
        self.coins = [coin for coin in self.coins if coin.x > self.camera_x - 200]
    
    def check_collisions(self):
        player_rect = pygame.Rect(self.player.x, self.player.y, 
                                self.player.width, self.player.height)
        
        # コインとの衝突
        for coin in self.coins:
            if not coin.collected:
                coin_rect = pygame.Rect(coin.x - coin.radius, coin.y - coin.radius,
                                      coin.radius * 2, coin.radius * 2)
                if player_rect.colliderect(coin_rect):
                    coin.collected = True
                    self.score += 10
    
    def update(self):
        if not self.game_over:
            # プレイヤーの更新
            game_over = self.player.update(self.terrain)
            if game_over:
                self.game_over = True
            
            # プレイヤーを右に移動
            self.player.x += self.player.speed
            
            # カメラの更新(プレイヤーを追従)
            self.camera_x = self.player.x - 200
            
            # 地形の更新
            self.terrain.update(self.camera_x)
            
            # コインの生成と管理
            self.generate_coins()
            
            self.check_collisions()
            self.distance = int(self.player.x / 10)
    
    def draw_ui(self):
        # スコア表示
        score_text = self.font.render(f"Score: {self.score}", True, BLACK)
        self.screen.blit(score_text, (10, 10))
        
        # 距離表示
        distance_text = self.font.render(f"Distance: {self.distance}m", True, BLACK)
        self.screen.blit(distance_text, (10, 50))
        
        # 操作説明
        if self.distance < 50:  # 最初の5秒間だけ表示
            help_text = pygame.font.Font(None, 24).render("Press SPACE to jump!", True, BLACK)
            self.screen.blit(help_text, (10, 90))
        
        if self.game_over:
            game_over_text = self.font.render("GAME OVER - Press R to Restart", True, RED)
            text_rect = game_over_text.get_rect(center=(SCREEN_WIDTH//2, SCREEN_HEIGHT//2))
            self.screen.blit(game_over_text, text_rect)
    
    def draw(self):
        self.screen.fill((135, 206, 235))  # 空の色
        
        # 地形の描画
        self.terrain.draw(self.screen, self.camera_x)
        
        # プレイヤーの描画
        self.player.draw(self.screen, self.camera_x)
        
        # コインの描画
        for coin in self.coins:
            coin.draw(self.screen, self.camera_x)
        
        self.draw_ui()
        
        pygame.display.flip()
    
    def restart(self):
        self.player = Player(100, SCREEN_HEIGHT - 150)
        self.terrain = Terrain()
        self.coins = []
        self.camera_x = 0
        self.score = 0
        self.distance = 0
        self.game_over = False
        self.coin_spawn_timer = 0
    
    def run(self):
        running = True
        
        while running:
            for event in pygame.event.get():
                if event.type == pygame.QUIT:
                    running = False
                elif event.type == pygame.KEYDOWN:
                    if event.key == pygame.K_SPACE:
                        if not self.game_over:
                            self.player.jump()
                    elif event.key == pygame.K_r and self.game_over:
                        self.restart()
            
            self.update()
            self.draw()
            self.clock.tick(60)
        
        pygame.quit()

if __name__ == "__main__":
    game = Game()
    game.run()

最後に

このように、Amazon Q DeveloperとPygameを利用すれば簡単にゲームが作れますので、皆さんも一度試してみて頂ければと思います。

参考になりましたら幸いです。

Discussion