Arcadeでゲームプログラミングの準備

Arcadeは、Pythonを使って絵を描いたり、キャラクターを動かしたりして、 2Dゲームやアニメーションを作るためのライブラリです。Scratchで学んだ考え方をそのまま活かせるので、 Pythonを楽しく学び始めるのにぴったりです。

Scratchでは、ブロックパレットからブロック(プログラムの命令)を選び、 スクリプトエリアに並べます。ブロックを組み合わせることで、 スプライト(キャラクター)の動きやゲームの動作を作ることができます。

一方、Pythonでは、ブロックの代わりに文字や記号を使って命令を書きます。 このように、プログラムを書くための文字や記号の並びを「コード」と呼びます。 Scratchでブロックをつなげていくように、Pythonでは命令を表すコードを上から順番に書き並べて、 コンピューターにしてほしいことを伝えます。たとえば、キャラクターを動かしたいときは、「右に進む」 「10歩動く」といった動きを、Pythonの命令を使ってコードにします。

今回は、ボールが壁にぶつかると跳ね返るプログラムを作ります。

なを、このページは、Arcade Academy - Learn Python「18. Using the Window Class」参照し、 実際にコードを動かして試しながら書いています。

目次

ゲームを動かす仕組み
(1) MyGameを定義し、初期化する
(2) 描画の更新
(3) アニメーション
(4) 壁で跳ね返るボール
(5) ボールの数を増やす
(6) スプライトを表示する
(7) マウスを追いかける
(8) ボタンを押すとゲームスタート
(9) 回転方向を左右のみにする
(11) 敵のスプライトに当たったら音が出る
(12) 背景の変更
(13) 同心円上にボールを放出
(14) ジャンプで敵を避ける

上の(1)から(13)のサンプルプログラムをこちらのGithubリポジトリにあります。

ゲームを動かす仕組み

Arcadeで2Dの絵を描くプログラムはPython Arcade「絵を描く方法」にも書きました。 次のコードでWindowを開いて絵を描きました。このプログラムでは幅640、高さ480のWindowを開いています。

import arcade
arcade.open_window(640, 480, "描画サンプル") # Width x Height
arcade.run()

これで、640×480ピクセルのゲーム画面を作ることができます。でも、画面ができただけでは、まだゲームにはなっていません。 ゲームを作るには、

  1. キャラクターを画面に表示する
  2. キャラクターを動かす
  3. キーボードで操作する
  4. 敵を動かす
  5. 当たり判定をする
  6. 点数を計算する

など、たくさんの仕組みが必要です。 そこで、ArcadeのWindowをそのまま使うのではなく、ゲームに必要な機能を追加した、自分専用のWindowを作ります。 そのために使うのが、

class MyGame(arcade.Window):

という書き方です。 これは、Arcadeが用意したWindowを土台にして、自分のゲーム用のMyGameを作るという意味です。 このクラスは、「キャラクターを画面に表示する」などゲームを作る上で必要な機能を持っています。

(1) MyGameを定義し、初期化する

__init__(self, width, height, title)は、 このクラスを作成した時に、一番最初に実行される関数です。 この中で、super().__init__(width, height, title)は親である arcade.Window の初期設定を実行しています。

import arcade

# MyGame クラスの定義 ---
class MyGame(arcade.Window):
    
    def __init__(self, width, height, title):
        # 親クラスの初期化関数をコールする
        super().__init__(width, height, title)

# --- クラスの定義終わり

# MyGame を作成
mywindow = MyGame(640, 480, "MyGame Example")
arcade.run()

(2) 描画の更新

on_draw(self)は1秒間に約60回、定期的に呼ばれる関数です。 この関数で再描画してアニメーションを作成します。 この中でself.clear()で背景以外を全部消して、その上で draw_lrbt_rectangle_filledで Windowの下の4分の1を緑色に塗り、さらにボールを draw_circle_filledで描き加えています。この場合は ボールが毎回、同じ位置に描画されるので、描画が更新されても静止画になります。



import arcade

# MyGame クラスの定義 ---
class MyGame(arcade.Window):
    
    def __init__(self, width, height, title):
        # 親クラスの初期化関数をコールする
        super().__init__(width, height, title)
        # 背景色
        arcade.set_background_color(arcade.color.SKY_BLUE)

    # 1秒間に60回、この関数が呼び出され再描画します。
    def on_draw(self):
        self.clear()    #全部消す
        arcade.draw_lrbt_rectangle_filled(0, 640, 0, 240, arcade.csscolor.GREEN)
        arcade.draw_circle_filled(50, 300, 15, arcade.color.AUBURN)

# --- クラスの定義終わり

# MyGame を作成
mywindow = MyGame(640, 480, "MyGame Example")
arcade.run()

(3) アニメーション

ボールのX座標を self.ball_x = 0 と定義します。 on_update(self, delta_time) は1秒間に約60回、定期的に呼ばれる関数です。この暗数の中でこのX座標をアップデートします。 右端に到達したら、左端に戻すようにしています。



import arcade

# MyGame クラスの定義 ---
class MyGame(arcade.Window):
    
    def __init__(self, width, height, title):
        # 親クラスの初期化関数をコールする
        super().__init__(width, height, title)
        # 背景色
        arcade.set_background_color(arcade.color.SKY_BLUE)
        self.ball_x = 0

    # 1秒間に60回、この関数が呼び出され再描画します。
    def on_draw(self):
        self.clear()
        arcade.draw_lrbt_rectangle_filled(0, 640, 0, 120, arcade.csscolor.GREEN)
        arcade.draw_circle_filled(self.ball_x, 160, 15, arcade.color.AUBURN)

    # 1秒間に60回、この関数が呼び出されます。
    def on_update(self, delta_time):
        self.ball_x += 2
        if(self.ball_x>=640):
            self.ball_x=0

# --- クラスの定義終わり

# MyGame を作成
mywindow = MyGame(640, 480, "MyGame Example")
arcade.run()

(4) 壁で跳ね返るボール

Scratchでは、「もし端に着いたら、跳ね返る」というブロックを使って、ボールを簡単に跳ね返すことができます。

Scratch での動き

MyGameでは、ボールのX方向とY方向の移動量を、それぞれ self.xinc、self.yinc という変数で管理します。 ボールが壁にぶつかったとき、どの壁にぶつかったかによって、跳ね返った後の動きを変えます。 左右の壁にぶつかった場合は、X方向の移動量を反転します。
self.xinc = -self.xinc
上下の壁にぶつかった場合は、Y方向の移動量を反転します。
self.yinc = -self.yinc
このように、Scratchの「跳ね返る」という動きを、Pythonでは「移動量を反転する」という処理に置き換えて考えることができます。

MyGameでの動き



import arcade
import numpy as np

Angle = 45.*np.pi/180

# MyGame クラスの定義 ---
class MyGame(arcade.Window):
    
    def __init__(self, width, height, title):
        # 親クラスの初期化関数をコールする
        super().__init__(width, height, title)
        # 背景色
        arcade.set_background_color(arcade.color.SKY_BLUE)
        self.xinc = 4*np.cos(Angle)
        self.yinc = 4*np.sin(Angle)
        self.ball_x = 50
        self.ball_y = 160

    # 1秒間に60回、この関数が呼び出され再描画します。
    def on_draw(self):
        self.clear()
        arcade.draw_lrbt_rectangle_filled(0, 640, 0, 120, arcade.csscolor.GREEN)
        arcade.draw_circle_filled(self.ball_x, self.ball_y , 15, arcade.color.AUBURN)

    # 1秒間に60回、この関数が呼び出されます。
    def on_update(self, delta_time):
        self.ball_x += self.xinc
        self.ball_y += self.yinc
        if(self.ball_x>=640):       # 右の壁にぶつかった
            self.xinc=-self.xinc    # 向きを左へ
        if(self.ball_x<=0):      # 左の壁にぶつかった
            self.xinc=-self.xinc    # 向きを右へ
        if(self.ball_y>=480):       # 上の壁にぶつかった
            self.yinc=-self.yinc    # 向きを下へ
        if(self.ball_y<=0):      # 下の壁にぶつかった
            self.yinc=-self.yinc    # 向きを上へ

# --- クラスの定義終わり

# MyGame を作成
mywindow = MyGame(640, 480, "MyGame Example")
arcade.run()

(5) ボールの数を増やす

ボールをいくつも作って動かすために、ボールの設計図となる「ボールクラス」を作ります。 ボールクラスには、それぞれのボールが持っている情報として、位置、色、大きさ、X方向の移動量、Y方向の移動量などを用意します。

xposs: 最初の位置  X座標
yposs: 最初の位置  Y座標
bsize: ボールの大きさ
bsize: ボールの移動速度
bspeed: ボールの移動方向
bcolor ボールの色

ボールの位置を計算する処理と、ボールを画面に描くための描画処理も用意します。 ゲームが進むとon_update()からupdate()が呼び出され、 ボールの位置を計算し直します。そして、画面を描くときにはon_draw()から描画処理draw()が呼び出されます。 このように、ボールに必要な「情報」と「動作」をひとまとめにしておくことで、同じボールをいくつでも作ることができます。

import arcade
import numpy as np

# ボールのクラスの定義 ---
class Ball(arcade.Window):
    def __init__(self, xposs, yposs, bsize, bangle, bspeed, bcolor):
        self.ball_x = xposs
        self.ball_y = yposs
        self.ball_size = bsize
        self.xinc = bspeed*np.cos(bangle*np.pi/180)
        self.yinc = bspeed*np.sin(bangle*np.pi/180)
        self.color = bcolor

    def updata(self,delta_time):
        self.ball_x += self.xinc
        self.ball_y += self.yinc
        if(self.ball_x>=640):   # 右の壁にぶつかった
            self.xinc=-self.xinc
        if(self.ball_x<=0):     # 左の壁にぶつかった
            self.xinc=-self.xinc
        if(self.ball_y>=480):   # 上の壁にぶつかった
            self.yinc=-self.yinc
        if(self.ball_y<=0):     # 下の壁にぶつかった
            self.yinc=-self.yinc      

    def draw(self):
        arcade.draw_circle_filled(self.ball_x, self.ball_y , self.ball_size, self.color)
# --- クラスの定義終わり

# MyGame クラスの定義 ---
class MyGame(arcade.Window):
    
    def __init__(self, width, height, title):
        # 親クラスの初期化関数をコールする
        super().__init__(width, height, title)
        # 背景色
        arcade.set_background_color(arcade.color.SKY_BLUE)
        # ボールを創る
        self.ballr = Ball(50,  150, 10,  43, 12, arcade.color.RED)
        self.ballg = Ball(350, 360, 15, -42,  9, arcade.color.YELLOW)
        self.ballb = Ball(250, 250, 20,  64,  6, arcade.color.BLUE)

    # 1秒間に60回、この関数が呼び出され再描画します。
    def on_draw(self):
        self.clear()
        arcade.draw_lrbt_rectangle_filled(0, 640, 0, 120, arcade.csscolor.GREEN)
        self.ballr.draw()
        self.ballg.draw()
        self.ballb.draw()

    # 1秒間に60回、この関数が呼び出されます。
    def on_update(self, delta_time):
        self.ballr.updata(delta_time)
        self.ballg.updata(delta_time)
        self.ballb.updata(delta_time)
# --- クラスの定義終わり

# MyGame を作成
mywindow = MyGame(640, 480, "MyGame Example")
arcade.run()

(6) スプライトを表示する

スプライトを表示するには、表示するPNGファイルと大きさを指定して、 arcade.Sprite(image_source, size) を呼び出します。 その後、スプライトの初期位置を center_x、center_y で設定し、 必要に応じて angle で向きを設定します。 その上で、作成したスプライトを管理するための「リスト」に登録します。 on_draw() では、このリストに対して self.player_list.draw() を実行します。 すると、リストに登録されているすべてのスプライトがまとめて描画されます。 このように、スプライトをリストに登録しておくことで、 複数のスプライトをまとめて管理することができます。




ここで指定しているPNGファイルは、Arcadeにあらかじめ付属しているスプライトです。 ":resources:/images" は、Arcadeが用意している画像リソースを表します。 私の環境では、実際のファイルは次のディレクトリに保存されています。
~/Library/Python/3.12/lib/python/site-packages/arcade/resources/assets/images
Arcadeに付属しているスプライトや画像は、 公式ドキュメントのBuilt-In Resources のページで確認できます。このページから、ゲームに使用したいキャラクターやアイテム、 背景などの画像を探すことができます。

import arcade

# MyGame クラスの定義 ---
class MyGame(arcade.Window):
    
    def __init__(self, width, height, title):
        # 親クラスの初期化関数をコールする
        super().__init__(width, height, title)
        # 背景色
        arcade.set_background_color(arcade.color.SKY_BLUE)

        # スプライトの初期化
        image_source = ":resources:/images/space_shooter/playerShip1_blue.png"
        self.player_sprite = arcade.Sprite(image_source, 0.8)   # サイズ0.8
        self.player_sprite.center_x = 128
        self.player_sprite.center_y = 128
        self.player_sprite.angle = 0

        # 「リスト」はスプライトを管理するためのものです。スプライトは、いずれかのリストに入れる必要があります。
        self.player_list = arcade.SpriteList()
        self.player_list.append(self.player_sprite)

    # 1秒間に60回、この関数が呼び出され再描画します。
    def on_draw(self):
        self.clear()
        arcade.draw_lrbt_rectangle_filled(0, 640, 0, 120, arcade.csscolor.GREEN)
        self.player_list.draw() # スプライトの再描画
    # --- クラスの定義終わり

# MyGame を作成
mywindow = MyGame(640, 480, "MyGame Example")
arcade.run() 

(7) マウスを追いかける

Scratchでは、「マウスのポインターへ向ける」「マウスポインターへ行く」というブロックを使って、 スプライトがマウスを追いかけるというプログラムを創ることができます。



Scratchの場合

MyGameでは、n_mouse_motion(self, x, y, dx, dy)という関数を 使って、マウスの位置を知ることができます。この関数の中で、スプライトが向かう目的地self.destを設定します。 また、スプライトが60回のon_update()で目的地に到着するように、 目的地までの移動速度を計算し、スプライトの向きも設定します。on_update()では、 スプライトの現在位置から目的地までの距離を計算します。 まだ目的地まで距離がある場合は、スプライトのupdate()を呼び出して、 スプライトの位置を少しずつ更新します。この処理を繰り返すことで、スプライトがマウスの位置に向かって移動します。

MyGameの場合

import arcade
import numpy as np

# MyGame クラスの定義 ---
class MyGame(arcade.Window):
    
    def __init__(self, width, height, title):
        # 親クラスの初期化関数をコールする
        super().__init__(width, height, title)
        # 背景色
        arcade.set_background_color(arcade.color.SKY_BLUE)
        self.dest= [0,0]    # 目的地

        # スプライトの初期化
        image_source = ":resources:/images/space_shooter/playerShip1_blue.png"
        self.player_sprite = arcade.Sprite(image_source, 0.8)
        self.player_sprite.center_x = 64
        self.player_sprite.center_y = 128
        self.player_sprite.angle = 90

        # 「リスト」はスプライトを管理するためのものです。スプライトは、いずれかのリストに入れる必要があります。
        self.player_list = arcade.SpriteList()
        self.player_list.append(self.player_sprite)

    # 1秒間に60回、この関数が呼び出され再描画します。
    def on_draw(self):
        self.clear()
        arcade.draw_lrbt_rectangle_filled(0, 640, 0, 120, arcade.csscolor.GREEN)
        self.player_list.draw() # スプライトの再描画

    # マウスが動いたら x, y がマウスの位置
    def on_mouse_motion(self, x, y, dx, dy):
        self.dest = np.array([x,y]) # 目的地を設定
        ax = x - self.player_sprite.center_x
        ay = y - self.player_sprite.center_y
        self.player_sprite.change_x=ax/60   # 目的地までの移動速度
        self.player_sprite.change_y=ay/60   # 目的地までの移動速度
        self.player_sprite.angle = np.degrees(np.arctan2(ax,ay))    # 目的地の方向

    # 1秒間に60回、この関数が呼び出されます。
    def on_update(self, delta_time):
        pos = np.array([self.player_sprite.center_x,self.player_sprite.center_y])
        dist = np.linalg.norm(pos-self.dest)    # 目的地までの距離
        if(dist > 10):                          # 目的地までの距離が大きかったら
            self.player_sprite.update()         # 位置をアップデートする
    # --- クラスの定義終わり

# MyGame を作成
mywindow = MyGame(640, 480, "MyGame Example")
arcade.run()

(8) ボタンを押すとゲームスタート

Scratchでは、緑の旗を押してゲームを開始し、赤いボタンでゲームを停止するという操作をよく使います。 このMyGameでは、画面上に緑と赤のボタンを配置し、緑のボタンをマウスでクリックするとゲームを開始し、 赤のボタンをクリックするとゲームを停止するように作ります。

MyGameのクラスの中で、緑ボタンの初期化と赤のボタンを配置します。初期値ではゲームが停止状態 ですので、 self.player_sprite_red.color = (255, 255, 255) で赤のボタンを明るく、 self.player_sprite_green.color = (180, 180, 180) で緑のボタンを暗く表示しています。

on_mouse_pressは マウスクリックで呼ばれる関数で、その中でマウスの位置から緑ボタンがクリックされたのか 赤ボタンがクリックされたのかを判断しています。緑ボタンでstart_game()、 赤ボタンでstop_game()を呼び出しています。

この例では、start_gameでロケットのスプライトが左下から右上へ移動します。

import arcade
import numpy as np

# MyGame クラスの定義 ---
class MyGame(arcade.Window):
    
    def __init__(self, width, height, title):
        # 親クラスの初期化関数をコールする
        super().__init__(width, height, title)
        # 背景色
        arcade.set_background_color(arcade.color.SKY_BLUE)

        # 緑ボタンの初期化
        image_source = ":resources:/gui_basic_assets/window/panel_green.png"
        self.player_sprite_green = arcade.Sprite(image_source, 1.0)
        self.player_sprite_green.center_x = 640-64
        self.player_sprite_green.center_y = 480-32
        self.player_sprite_green.visible = True
        self.player_sprite_green.color = (180, 180, 180)    # 少し暗く表示

        # 赤ボタンの初期化
        image_source = ":resources:/gui_basic_assets/window/panel_red.png"
        self.player_sprite_red = arcade.Sprite(image_source, 1.0)
        self.player_sprite_red.center_x = 640-30
        self.player_sprite_red.center_y = 480-32
        self.player_sprite_red.visible = True
        self.player_sprite_red.color = (255, 255, 255)      # 明るく表示

        # ロケットの初期化
        image_source = ":resources:/images/space_shooter/playerShip1_blue.png"
        self.player_sprite = arcade.Sprite(image_source, 0.8)
        self.player_sprite.center_x = 64
        self.player_sprite.center_y = 128
        self.player_sprite.angle = 60
        self.player_sprite.change_x=2
        self.player_sprite.change_y=1
        self.dest = np.array([640,480])     # 目的地


        # 「リスト」はスプライトを管理するためのものです。スプライトは、いずれかのリストに入れる必要があります。
        self.player_list = arcade.SpriteList()
        self.player_list.append(self.player_sprite_green)
        self.player_list.append(self.player_sprite_red)
        self.player_list.append(self.player_sprite)

        # ゲームスタートフラグ
        self.game_started = False   # 停止中

    # 1秒間に60回、この関数が呼び出され再描画します。
    def on_draw(self):
        self.clear()
        arcade.draw_lrbt_rectangle_filled(0, 640, 0, 120, arcade.csscolor.GREEN)
        self.player_list.draw() # スプライトの再描画

    def on_mouse_press(self, x, y, button, modifiers):
        # 緑ボタンでクリック
        if self.player_sprite_green.collides_with_point((x, y)):
            self.start_game()   #ゲームスタート
            self.player_sprite_red.color = (180, 180, 180)
            self.player_sprite_green.color = (255, 255, 255)
            self.player_sprite.center_x = 64
            self.player_sprite.center_y = 128

        # 赤ボタンでクリック
        if self.player_sprite_red.collides_with_point((x, y)):
            self.stop_game()   #ゲームストップ
            self.player_sprite_red.color = (255, 255, 255)
            self.player_sprite_green.color = (180, 180, 180)

    def on_update(self, delta_time):
        if(self.game_started):
            pos = np.array([self.player_sprite.center_x,self.player_sprite.center_y])
            dist = np.linalg.norm(pos-self.dest)    # 目的地までの距離
            if(dist < 100):                         # 目的地までの距離が大きかったら
                self.player_sprite.center_x = 64
                self.player_sprite.center_y = 128
            self.player_sprite.update()         # 位置をアップデートする

    # ゲームスタート
    def start_game(self):
        self.game_started = True
        print("ゲーム開始")

    # ゲームスタート
    def stop_game(self):
        self.game_started = False
        print("ゲーム停止")

    # --- クラスの定義終わり

# MyGame を作成
mywindow = MyGame(640, 480, "MyGame Example")
arcade.run()