0
1

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を作る(11. 敵遭遇後処理)

Last updated at Posted at 2025-01-03

概要

RPGゲームにおいて、Mapの敵を倒したら、さらに新しい敵が湧かないと枯れてしまうため、以下の処理が必要となる

1.敵と遭遇したら、戦闘画面になり、その後、敵を消込を行う。
2.新たな敵を生成し、Mapに追加する

ロジック処理

1.空のMapデータにモンスター配置情報を更新する
2.Playerの位置情報を配置しないエリアを更新する
3.モンスターを1つ追加生成し、配置する
4.Map上常に同じ数の敵を維持する

ロジック処理で必要な情報

1.Playerの位置(モンスター遭遇時)
2.モンスター配置していない空のMapデータ(Blockは必要)
3.モンスター配置情報(こちらはPlayerに倒されるため、変動情報となる)

Playerクラス

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

        self.collision_sprites = collision_sprites
        self.enemy_sprites = enemy_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(-10,-10)

        # 移動関連
        self.key_speed = 5
        self.direction = pg.Vector2(0, 0)
        self.game_stage = 'main'
        self.enemy_name = ''

    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, enemy_list, mob_pos_info, 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, enemy in zip(self.enemy_sprites, enemy_list):
            if enemy_sprite.rect.colliderect(self.hit_box_rect):
                print(f'{enemy.name}と衝突しました!')
                self.game_stage = 'battle'
                self.enemy_name = enemy.name
                collided_enemies.append((enemy_sprite, enemy))

        # 衝突した敵を削除
        for enemy_sprite, enemy in collided_enemies:
            if enemy_sprite in self.enemy_sprites:
                self.enemy_sprites.remove(enemy_sprite)  # スプライトグループから削除
            if enemy in enemy_list:
                enemy_list.remove(enemy)  # インスタンスリストから削除

                # print(f'mobpos:{enemy.mob_pos}')
                if enemy.mob_pos in mob_pos_info:
                    mob_pos_info.remove(enemy.mob_pos)

                self.general_mob(base_map, mob_pos_info, enemy_list)
                # print(f'mob_pos_info:{mob_pos_info}')
            # enemy.destroy()  # 敵のリソースを解放

        # メイン矩形をヒットボックスに同期
        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, mob_pos_info, enemy_list):

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

        # Mob配置禁止エリアの計算
        self.non_mob_area = self.cal_mob_area(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)

        for row in self.place_result_map:
            print(row)

        # 配置可能な座標を取得
        zero_positions = [(i, j) for i, row in enumerate(self.place_result_map) for j, val in enumerate(row) if val == 0]
        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
            enemy = Enemy((y, x), id, flattened_list, self.enemy_sprites)
            enemy_list.append(enemy)
        else:
            print("モンスターを配置できるスペースがありません。")
    
    
    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, enemy_list, mob_pos_info, current_map):
        base_map = self.get_base_map(current_map)
        self.handle_input(dt)
        self.collision_block(dt)
        self.collision_enemy(dt, enemy_list, mob_pos_info, base_map)

Mapクラス

class Map(pg.sprite.Sprite):
    def __init__(self, collision_sprites, enemy_sprites):

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

        self.collision_sprites = collision_sprites
        self.enemy_sprites = enemy_sprites

        self.map_list = []
        self.enemy_list = []
        # mob配置はキューで管理する
        self.mob_pos_info = []
        self.player = None

    def create(self):
        
        # 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.map_list.append(self.block)
                if column == 'P':
                    self.player = Player((x,y), self.collision_sprites, self.enemy_sprites)
        
        # 敵の配置
        self.create_enemy()

        # 
        self.base_map = self.get_base_map()
        return self.player, self.map_list, self.enemy_list, self.mob_pos_info, 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:
                    print(f'x:{j} y:{i}{column}を配置')
                    self.mob_pos_info.append([j,i])

                    self.enemy = Enemy((x,y), id, [j,i], self.enemy_sprites)

                    id += 1
                    if id >2: id = 0
                    self.enemy_list.append(self.enemy)
        # print(self.mob_pos_info)

    def cal_player_area(self, max_y, max_x):
        # px 
        px = -1
        py = -1
        for i, row in enumerate(self.current_map):
            for j, column in enumerate(row):
                if column == 'P':
                    px = j
                    py = i
                    break

        # 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)

        return px_min, px_max, py_min, py_max
        
    def cal_non_enemy_area(self):

        map_data = self.get_base_map()
        # mapデータの最大値を取得する
        max_map_y = len(map_data)
        max_map_x = len(map_data[0])

        # Playerエリアの計算
        px_min, px_max, py_min, py_max = self.cal_player_area(max_map_x, max_map_y)

        # 敵を配置できるエリアの取得
        self.loc_enemy_area = []
        for i, row in enumerate(self.current_map):
            row_area = []
            for j, column in enumerate(row):
                if column == '.':
                    px = j
                    py = i
                    if (px > px_min and py > py_min) and (px < px_max and py < py_max):
                        row_area.append(1)
                    else:
                        row_area.append(0) 
                else:
                    row_area.append(1)
            self.loc_enemy_area.append(row_area)

        return self.loc_enemy_area

    def replace_zeros_with_nines(self):
        """リスト内の0をランダムに選択して指定された数を9に置き換える。"""
        # 0の座標を探す
        self.grid = self.cal_non_enemy_area()

        zero_positions = [(i, j) for i, row in enumerate(self.grid) for j, val in enumerate(row) if val == 0]
        
        # 指定された数だけランダムに選択
        if MAX_ENEMY_NUM > len(zero_positions):
            raise ValueError("0の数より置き換える数が多いです。")
        
        random_positions = random.sample(zero_positions, MAX_ENEMY_NUM)
        
        # 選択された位置を9に置き換える
        for i, j in random_positions:
            self.grid[i][j] = 9

    def get_base_map(self):
        base_map = []
        for i, row in enumerate(self.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

まとめ

今回は、モンスター配置において、Playerの周りに配置しない、モンスター同士が近くに配置しないなど、ロジックを組み込んだ。
若干バグがあるが、とりあえずは機能する

Playerクラスには、戦闘画面遷移、戦闘後のMobを消す、生成、配置
Mapクラスには、初期のMob配置を分けた。

課題:
MapクラスとPlayerが大きくなりすぎた。

0
1
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
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?