Webカメラの被写体で遊ぶどうぶつタワーバトル風ゲームを作った
作ったもの
Webカメラの被写体で遊ぶ、どうぶつタワーバトル風のゲームを作りました。
遊んでいる様子
(よろしければstarを頂けると幸いです)
有名なので知らない方は少ないとは思いますが元ネタのゲームはこちらです。
動機
- 物理演算を使った何かを作りたい
- 大学の学園祭で展示したら盛り上がりそうなものを作りたい
技術
Python Arcade Library
Python Arcade LibraryというPythonでの2Dゲーム開発用ライブラリを使用しました。
Pythonで2Dゲーム開発といえばPygameが有名ですが、公式ドキュメントのPygameとの比較にあるようにPygameよりArcadeの方が優れている点が多いように感じます。
たとえばスプライトのヒットボックス(当たり判定)を自動で設定してくれたり、
画面スクロールを簡単に実装できたりなど、
便利な機能が沢山あります。特に公式のチュートリアルやサンプルコードが充実しているのは、便利なArcadeの機能をすぐに活かせそうで初心者にとってはとても嬉しいです。
ただPygameに比べると知名度が非常に低いため、公式ドキュメント以外の情報を探すのが大変です。(特に日本語ユーザーが増えてほしいと願っています)
arcade.Window
を継承したクラスを用意し、以下のようにon_
で始まるメソッドをオーバーライドして使用します。__init__()
とは別にゲーム内パラメータの初期化用にsetup()
を用意し、ゲーム内の状態のリセットを簡単に行えるようにしています。
class MyGame(arcade.Window):
def __init__(self, camera_id):
super().__init__(SCREEN_WIDTH, SCREEN_HEIGHT, "Tower Battle")
self.set_update_rate(1 / 1000)
# 省略
self.setup()
def setup(self):
# 省略
def on_draw(self):
# 省略
def on_update(self, delta_time):
# 省略
def on_key_press(self, key, modifiers):
# 省略
def on_key_release(self, key, modifiers):
# 省略
if __name__ == "__main__":
MyGame(int(sys.argv[1]) if len(sys.argv) >= 2 else 0) # 使用するカメラのIDをコマンドライン引数から渡す
arcade.run()
OpenCV/Pillow
クロマキー透過処理で利用しました。処理の内容についてはほぼ参考記事そのままなのでそちらをご覧ください。
SCREEN_WIDTH = 1280
SCREEN_HEIGHT = 720
class Camera:
def __init__(self, camera_id):
self.capture = cv2.VideoCapture(camera_id)
self.count = 0
self.position = [SCREEN_WIDTH / 2, 650]
self.angle = 0
def update(self):
self.sprite = arcade.Sprite()
self.sprite.position = self.position
self.sprite.angle = self.angle
ret, frame_image_cv = self.capture.read()
frame_image_cv = cv2.resize(frame_image_cv, (320, 180))
frame_image_cv = cv2.cvtColor(frame_image_cv, cv2.COLOR_RGB2RGBA)
# HSV変換
hsv = cv2.cvtColor(frame_image_cv, cv2.COLOR_BGR2HSV)
# 2値化
bin_img = ~cv2.inRange(hsv, (62, 100, 0), (79, 255, 255))
# 輪郭抽出
contours = cv2.findContours(
bin_img, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE
)[1]
# 面積が最大の輪郭を取得
if not contours: # 何も写っていない場合
self.sprite.texture = None
return
contour = max(contours, key=lambda x: cv2.contourArea(x))
# マスク画像作成
mask = np.zeros_like(bin_img)
cv2.drawContours(mask, [contour], -1, color=255, thickness=-1)
# 幅、高さは前景画像と背景画像の共通部分をとる
w = frame_image_cv.shape[1]
h = frame_image_cv.shape[0]
# 合成する領域
fg_roi = frame_image_cv[:h, :w]
bg_roi = np.zeros((h, w, 4), np.uint8)
# 合成
frame_image_cv = np.where(mask[:h, :w, np.newaxis] == 0, bg_roi, fg_roi)
frame_img_pil = cv2pil(frame_image_cv)
self.sprite.texture = arcade.Texture(
name=f"{self.count}", image=frame_img_pil, hit_box_algorithm="Detailed"
)
def draw(self):
if self.sprite.texture:
self.sprite.draw()
def move_x(self, change_x):
self.position[0] += change_x
self.position[0] = max(self.position[0], 0)
self.position[0] = min(self.position[0], 1280)
def move_y(self, change_y):
self.position[1] += change_y
self.position[1] = max(self.position[1], 650)
def rotate(self, change_angle):
self.angle += change_angle
def get_sprite(self):
self.count += 1
return self.sprite
def cv2pil(image):
# OpenCV型 -> PIL型
new_image = image.copy()
if new_image.ndim == 2: # モノクロ
pass
elif new_image.shape[2] == 3: # カラー
new_image = cv2.cvtColor(new_image, cv2.COLOR_BGR2RGB)
elif new_image.shape[2] == 4: # 透過
new_image = cv2.cvtColor(new_image, cv2.COLOR_BGRA2RGBA)
new_image = Image.fromarray(new_image)
return new_image
Pymunk
PymunkはPythonでの2D物理演算ライブラリです。Arcadeと組み合わせることで物理演算を使ったゲームの作成が可能になります。
最初にpymunk.Space
を作成し、足場となるステージを追加しておきます。
# Pymunk
self.space = pymunk.Space()
self.space.gravity = (0.0, -400.0)
# Terrain
self.terrain = arcade.Sprite(
filename="terrain.png",
center_x=SCREEN_WIDTH / 2,
center_y=SCREEN_HEIGHT / 2,
)
body = pymunk.Body(body_type=pymunk.Body.STATIC)
body.position = self.terrain.position
shape = pymunk.Poly(body, self.terrain.texture.hit_box_points)
shape.friction = 1
shape.elasticity = 0
self.space.add(body, shape)
カメラの現在の被写体からpymunk.Body
を生成するメソッドを用意して、任意のタイミングでスプライトを生成できるようにします。
def generate_sprite(self, sprite):
if not sprite.texture:
return
mass = 0.5
inertia = pymunk.moment_for_poly(mass, sprite.texture.hit_box_points)
body = pymunk.Body(mass, inertia)
body.position = sprite.position
body.angle = math.radians(sprite.angle)
shape = pymunk.Poly(body, sprite.texture.hit_box_points)
shape.friction = 1
shape.elasticity = 0
self.space.add(body, shape)
sprite.pymunk_shape = shape
self.object_list.append(sprite)
後は1フレーム毎にself.space.step(1 / 60)
を呼び出してあげればSpace上の全ての物体が更新されます。あとは画面外に物体が落下したらゲームオーバーなどの処理を実装すればゲームは完成です。
タイルセット
こちらのタイルセット素材を使わさせて頂きました。
(おまけ)Webカメラとクロマキー合成を使ったアプリを作るときのTips
プログラムから直接Webカメラの画像を取得して処理しようとすると、素人が用意した環境では照明や光の反射などのせいで大抵うまくいかないことが多いので、OBS Studioなどの仮想カメラを経由させてプレビューを見ながら色補正のパラメータを調整してあげるとうまくいきます。また仮想カメラを使用することで画像を用意しておけば物理的に環境を整える必要がなくなるので動作確認が楽になります。
おわりに
n番煎じですが物理演算を使った複数人で楽しめそうなゲームを作ることができました。またPython Arcade Libraryのユーザーが増えることを願っております。この記事がどなたかの参考になれば幸いです。
Discussion