概要
ソースコードを整理:
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への理解を深めることができたので、時間の無駄ではなかったと思う。