0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Pygameを使って RPGを作る(12. ソースコード整理)

Posted at

概要

ソースコードを整理:
Spriteによるクラス内のメンバー変数を使用できることが分かったので、クラス間のメンバー変数にアクセスする場合は、Spriteを経由で記述を簡素化した。
1.メインクラスの記述の簡素化
2.Mapクラスの戻り値をplayerとcurrent_mapだけ残す、スプライトグループ管理
3.Playerクラスを整理、外部引数はcurrent_mapのみ(いずれは、マップ管理クラス化するため、この引数もなくす予定)

メインクラス

今回の修正でかなり簡素化

class game:
    def __init__(self):
        pg.init()

        # screen
        self.display_surface = pg.display.set_mode((WIDTH, HEIGHT))
        # title
        pg.display.set_caption(TITLE)
        # clock
        self.clock = pg.time.Clock()
        self.running = True

        # all sprite
        self.all_sprites = AllSprites()

        self.game_stage = 'main'

        # Map
        self.player, self.current_map = Map(self.all_sprites).create()
        
    def events(self):
        # イベント処理
        for event in pg.event.get():
            if event.type == pg.QUIT:
                self.running = False
            if event.type == pg.KEYDOWN and event.key == pg.K_SPACE:
                    self.player.game_stage = "main"  # 状態を"main"に戻す

    def main_screen(self, dt):
        self.all_sprites.update(dt, self.current_map)
        self.display_surface.fill(BLUE)
        self.all_sprites.draw()

    
    def battle_screen(self):
        pg.display.set_caption('Battle')
        self.display_surface.fill((125, 125, 0))  # 背景を赤に
        font = pg.font.SysFont("yumincho", 74)
        text = font.render(f"{self.player.enemy_name}に遭遇!", True, (255, 255, 255))
        self.display_surface.blit(text, (250, 250))

    def run(self):
        """ゲームループ"""
        dt = self.clock.tick(FPS) / 1000

        while self.running:
            # events
            self.events()
            self.game_stage = self.player.game_stage
            if self.game_stage == 'main':
                self.main_screen(dt)

            elif self.game_stage == 'battle':
                # 敵と衝突した後の画面
                self.battle_screen()
            pg.display.update()

        pg.quit()

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

Mapクラス

Enemyインスタンスリスト、Mob位置情報、Blockインスタンスリストを削除できたので、かなりすっきり

class Map(pg.sprite.Sprite):
    def __init__(self, all_sprites, *groups):
        super().__init__(*groups)

        self.block_images = {
            "B" : "../maps/tree.png"
        }
        self.name = 'map_01'
        self.current_map = TILE[self.name]

        # 通過不可Sprite
        self.collision_sprites = pg.sprite.Group()

        self.enemy_sprites = pg.sprite.Group()

        self.all_sprites = all_sprites

    def create(self):
        
        # 敵の配置
        self.create_enemy()

        # BlockとPlayerの配置
        for i, row in enumerate(self.current_map):
            for j, column in enumerate(row):
                x = j * TILE_SIZE
                y = i * TILE_SIZE

                if column == 'B':
                    self.block = Block((x,y), self.block_images['B'], self.collision_sprites, self.all_sprites)

                if column == 'P':
                    self.player = Player((x,y), self.collision_sprites, self.enemy_sprites, self.all_sprites)

        return self.player, self.current_map
    
    def create_enemy(self):
        id = 0 #生成時作業用ID
        # 敵の配置
        self.replace_zeros_with_nines()
        for i, row in enumerate(self.grid):
            # print(row)
            for j, column in enumerate(row):
                x = j * TILE_SIZE
                y = i * TILE_SIZE
                if column == 9:
                    self.enemy = Enemy((x,y), id, [j,i], self.enemy_sprites, self.all_sprites)

                    id += 1
                    if id >2: id = 0

Playerクラス

これは大きく変更した。Enemyインスタンスリスト、Mob位置情報をSprite経由でできるのは本当に素晴らしいが、初めての人は理解するまでは、難しいかと思います。

from settings import * 
import pygame as pg
import random
from wall import Enemy

class Player(pg.sprite.Sprite):
    
    def __init__(self, pos, collision_sprites, enemy_sprites, all_sprites, *groups):
        super().__init__(*groups)

        self.collision_sprites = collision_sprites
        self.enemy_sprites = enemy_sprites
        self.all_sprites = all_sprites

        # イメージの読み込み
        self.surface = pg.image.load(IMAGE_PATH).convert_alpha()

        # 矩形(rect)と位置情報
        self.rect = self.surface.get_frect(topleft=pos)
        self.hit_box_rect = self.rect.inflate(1,1)

        # 移動関連
        self.key_speed = 10
        self.direction = pg.Vector2(0, 0)
        self.game_stage = 'main'
        self.enemy_name = ''
        # self.player_flag = True
        self.all_sprites.add(self)


    def handle_input(self, dt):
        """キーボード入力で移動処理を行う"""
        keys = pg.key.get_pressed()

        # キーが押されたときだけ移動
        self.direction.x = int(keys[pg.K_RIGHT]) - int(keys[pg.K_LEFT])
        self.direction.y = int(keys[pg.K_DOWN]) - int(keys[pg.K_UP])
        
        self.direction = self.direction.normalize() if self.direction else self.direction

    def collision_enemy(self, dt, base_map):

        collided_enemies = []  # 衝突した敵を記録するリスト

        # 水平方向、垂直方向の衝突判定を処理
        self.hit_box_rect.x += self.direction.x * self.key_speed * dt
        self.hit_box_rect.y += self.direction.y * self.key_speed * dt

        for enemy_sprite in self.enemy_sprites:
            if enemy_sprite.rect.colliderect(self.hit_box_rect):
                print(f'{enemy_sprite.name}と衝突しました!')
                self.game_stage = 'battle'
                self.enemy_name = enemy_sprite.name
                collided_enemies.append((enemy_sprite)) #あとで集計ログで使うので残す
        
        # Mobの位置を取得
        self.mob_pos_info = [enemy.mob_pos for enemy in self.enemy_sprites]

        # 衝突した敵を削除
        for enemy_sprite in collided_enemies:
            if enemy_sprite in self.enemy_sprites:
                # スプライトグループから削除
                enemy_sprite.kill()

        if len(self.mob_pos_info) < MAX_ENEMY_NUM:
            # Mob生成
            self.general_mob(base_map)
            # self.enemy_sprites.append(new_enemy)
        
        # メイン矩形をヒットボックスに同期
        self.rect.center = self.hit_box_rect.center

    def cal_mob_area(self, mob_pos_info, max_y, max_x):

        all_list = []
        for px, py in mob_pos_info:

            # NONエリアの計算
            px_min = max(0, px - MOB_AREA)
            px_max = min(max_x - 1, px + MOB_AREA)
            py_min = max(0, py - MOB_AREA)
            py_max = min(max_y - 1, py + MOB_AREA)

            mob_area = []
            for p_y in range(py_min, py_max + 1):
                for p_x in range(px_min, px_max + 1):
                    mob_area.append([p_x, p_y])  # 各座標をリストに追加
            
            all_list.extend(mob_area)

        return all_list
    
    def place_exits_mob(self, map_data, mob_pos_info, num):
        max_y = len(map_data)
        max_x = len(map_data[0])

        for x, y in mob_pos_info:
            # 範囲外アクセスを防ぐチェック
            # if 0 <= y < max_y and 0 <= x < max_x:
            if map_data[y][x] == 0:  # 配置可能な場所
                map_data[y][x] = num  # モンスターを配置
            else:
                # print(f"座標 ({x}, {y}) にモンスターを配置できません。")
                pass
            # else:
            #     print(f"座標 ({x}, {y}) はマップの範囲外です。")
        return map_data

    def get_base_map(self, current_map):
        base_map = []
        for i, row in enumerate(current_map):
            row_area = []
            for j, column in enumerate(row):
                if column == 'B':
                    row_area.append(1) 
                else:
                    row_area.append(0)
            base_map.append(row_area)

        return base_map

    def general_mob(self, map_data):

        max_map_y = len(map_data)
        max_map_x = len(map_data[0])

        # Mob配置禁止エリアの計算
        self.non_mob_area = self.cal_mob_area(self.mob_pos_info, max_map_y, max_map_x)

        # 既存のMobを配置する
        self.updated_map = self.place_exits_mob(map_data, self.non_mob_area, 9)

        # 配置禁止エリアの計算
        p = self.cal_player_area(max_map_y, max_map_x)
        self.place_result_map = self.place_exits_mob(self.updated_map, p, 3)

        # 配置可能な座標を取得
        zero_positions = [(i, j) for i, row in enumerate(self.place_result_map) for j, val in enumerate(row) if val == 0]
        # print(zero_positions)
        if zero_positions:
            new_mob = random.sample(zero_positions, 1)
            flattened_list = [item for tup in new_mob for item in tup]
            print(f'配置したモンスター: {flattened_list}')
            # ランダムでMobを生成
            id = random.randint(0, 2)
            x = flattened_list[0] * TILE_SIZE
            y = flattened_list[1] * TILE_SIZE

            # Mob生成
            Enemy((y, x), id, flattened_list, self.enemy_sprites, self.all_sprites)
        else:
            print("モンスターを配置できるスペースがありません。")

        # return new_enemy
    
    def cal_player_area(self, max_y, max_x):
        # プレイヤーのタイル位置を計算
        px = int(self.rect.centerx / TILE_SIZE)
        py = int(self.rect.centery / TILE_SIZE)

        # NONエリアの計算
        px_min = max(0, px - NON_ENEMY_AREA)
        px_max = min(max_x - 1, px + NON_ENEMY_AREA)
        py_min = max(0, py - NON_ENEMY_AREA)
        py_max = min(max_y - 1, py + NON_ENEMY_AREA)

        player_area = []
        for p_y in range(py_min, py_max + 1):
            for p_x in range(px_min, px_max + 1):
                player_area.append([p_x, p_y])  # 各座標をリストに追加

        return player_area

    def collision_block(self,dt):
        """ブロックとの衝突判定と位置調整を行う"""
        # 水平方向の衝突処理
        self.hit_box_rect.x += self.direction.x * self.key_speed *dt
        for block in self.collision_sprites:
            if block.rect.colliderect(self.hit_box_rect):
                if self.direction.x > 0:  # 右に移動中
                    self.hit_box_rect.right = block.rect.left
                elif self.direction.x < 0:  # 左に移動中
                    self.hit_box_rect.left = block.rect.right
                # 衝突後、移動量をリセット
                self.direction.x = 0

        # 垂直方向の衝突処理
        self.hit_box_rect.y += self.direction.y * self.key_speed *dt
        for block in self.collision_sprites:
            if block.rect.colliderect(self.hit_box_rect):
                if self.direction.y > 0:  # 下に移動中
                    self.hit_box_rect.bottom = block.rect.top
                elif self.direction.y < 0:  # 上に移動中
                    self.hit_box_rect.top = block.rect.bottom
                # 衝突後、移動量をリセット
                self.direction.y = 0

        # メイン矩形をヒットボックスに同期
        self.rect.center = self.hit_box_rect.center

    def update(self, dt, current_map):
        base_map = self.get_base_map(current_map)
        self.handle_input(dt)
        self.collision_block(dt)
        self.collision_enemy(dt, base_map)

AllSpritesクラス

新設したクラス、このクラスでBlock、Enemy、Playerを一つのDrawメソッドで更新できるようにした。
コード量はちょっとだけだけど、影響度合いが非常に大きい。

class AllSprites(pg.sprite.Group):
    def __init__(self, *sprites):
        super().__init__(*sprites)
        
        # display
        self.display_surface = pg.display.get_surface()

        self.offset =pg.Vector2()

    def draw(self):

        for sprite in self:
            self.display_surface.blit(sprite.surface, sprite.rect.topleft)

感想

今回は、Enemyが生成されない、生成したEnemyが見えない。Enemyは見えたが、今度はPlayerが表示されないなど、かなり苦労しましたが、最終的にSpriteへの理解を深めることができたので、時間の無駄ではなかったと思う。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?