このページでは、Python で tkinter を利用した簡単な「横スクロールアクションゲーム」の作り方を解説していきます。
このページは「横スクロールアクションゲームの作り方の解説」の9ページ目となります。「横スクロールアクションゲームの作り方」の解説ページとしては最後のページとなります。
8ページ目は下記ページとなり、8ページ目では主に詳細な当たり判定を行う方法の解説を行なっています。
【Python/tkinter】横スクロールアクションゲームを作る(より詳細な当たり判定)また、このページでは、上記ページの このページで作成したスクリプト で紹介しているスクリプトを変更していきながらゲームを作成していきます。
ですので、事前に上記のページを読んでおくことをオススメします。
このページでは、今まで作成してきた横スクロールアクションゲームのカスタマイズ例を紹介していきます。
具体的に紹介するカスタマイズ例は下記のようになります。
定義値を変更するだけで行えるカスタマイズもあれば、スクリプトをがっつり変更して機能追加するものもあります。
これらはあくまでもカスタマイズの一例です。
おそらく、ここまで横スクロールアクションゲームの作り方の解説を読んできてくださった方であれば、動作や見た目に不満があったり、もっといろんな機能を追加してみたいと思ってくださっているのではないかと思います。
ぜひ、このページで紹介するカスタマイズ例を参考にして、ご自身が持たれている不満や要望をご自身の手でプログラミングして解決してみてください!
こういったことを繰り返していけば、自然とプログラミング力もみについていくと思います!
Contents
敵キャラクターの数やゲーム画面のサイズ変える
まずはグローバル変数の値を変更するだけで行える、一番簡単なカスタマイズ例から紹介していきます。
敵キャラクターの数を変更する
ここまで作成してきたゲームでは、スクリプト先頭部分で設定しているグローバル変数の値を変更することでゲームのカスタマイズが行えるようになっています。
例えば敵キャラクターの数も、スクリプト先頭部分で設定しているグローバル変数の値により変更可能です。
具体的には、下記の NUM_CAT_ENEMY
の数を変更すれば猫型敵キャラクターの数を、NUM_DOG_ENEMY
の数を変更すれば犬型敵キャラクターの数を変更することが可能です。
NUM_CAT_ENEMY = 3
NUM_DOG_ENEMY = 3
ただし、敵キャラクターの数が多過ぎると当たり判定や画像の描画の負荷が大きくなりすぎてゲームが起動しなくなったりゲームの動きがガタガタしたりするので注意してください。
スポンサーリンク
ゲーム画面のサイズを変更する
また、下記の GAME_WIDTH
では、ゲーム画面の横幅を変更することができますので、ステージの長さを長くしたいのであれば、ここの値を大きくするだけで実現することができます。
GAME_WIDTH = 1500
VIEW_WIDTH
や VIEW_HEIGHT
を変更すれば表示領域のサイズも変更できますし、特に VIEW_HEIGHT
を変更することで同時にゲーム画面の高さも変更されますので、縦長のゲームにしたい場合などに変更してみてください。
背景画像の縦横比を保つ
先ほど ゲーム画面のサイズを変更する でゲーム画面のサイズを変更可能であることを説明しましたが、ゲーム画面の高さに対してゲーム画面の幅が大きすぎると背景画像が横に伸びてしまい、見栄えが悪くなる可能性があるので注意してください。
もし使用している背景画像が反転しても見た目にそこまで影響ないものであれば、Screen
クラスの drawBackground
メソッドを下記のように変更すれば、この見栄えが悪くなる現象は解決することができると思います。
class Screen:
def drawBackground(self):
image = Image.open(BG_IMAGE_PATH)
#↓これは削除
#size = (self.game_width, self.game_height)
#resized_image = image.resize(size)
#self.bg_image = ImageTk.PhotoImage(resized_image)
#↑これは削除
#↓これを追加
ratio = self.game_height / image.height
resized_image = image.resize((round(ratio * image.width), round(ratio * image.height)))
import math
num_image = math.ceil(self.game_width / resized_image.width)
bg_image = Image.new(resized_image.mode, (num_image * resized_image.width, self.view_height))
for i in range(num_image):
if i % 2 == 0:
bg_image.paste(resized_image, ((i * resized_image.width), 0))
else:
bg_image.paste(ImageOps.mirror(resized_image), ((i * resized_image.width), 0))
self.bg_image = ImageTk.PhotoImage(bg_image)
#↑これを追加
self.canvas.create_image(
0, 0,
anchor=tkinter.NW,
image=self.bg_image
)
上記の drawBackground
メソッドで何をやっているかを簡単に説明すると、まず resize
メソッド実行部分で背景画像を画像の縦横比を保ったままゲーム画面の高さに合わせて拡大縮小しています。
続いて、その拡大縮小後の画像がゲーム画面を埋め尽くすのに何枚必要であるかをカウントしています(切り上げするための関数 ceil
を利用するために import math
を行なっています。メソッド内で import
していますが、ファイルの先頭で import
した方が良いです)。
さらに、そのカウントした枚数分の画像が入り切るようなサイズの画像オブジェクトを Image.new
により生成しています。
そして、その生成したオブジェクトに拡大縮小後の画像を横に並べるように貼り付けていっています。この画像の貼り付けを行なっているのが paste
メソッド実行部分になります。
そのまま単純に横に並べてしまうと、下の図のように画像の端部分がガタガタになってしまう可能性があるので、それを防ぐために左右反転前後の画像を交互に並べるようにして貼り付けるようにしています。
画像の左右反転を行なっているので文字などがあるとむしろゲームの背景がおかしくなってしまいますが、今回用意したような背景向けの画像であれば、自然に画像が並べられると思います。
ボタンで「ゲーム開始」「停止」「リセット」を行う
現状では、ゲームはスクリプト実行直後から始まってしまいますし、ゲームが始まったら停止もできません。また、ゲームを最初からやり直そうと思うとスクリプトを終了してから再度実行する必要があります。
ここでは、これらの「ゲーム開始」「停止」「リセット」をボタン操作で行えるようにゲームをカスタマイズしていきたいと思います。
ボタンを作成する
まずはこれらを操作するためのボタンを作成したいと思います。
ボタンの作成は Screen
クラスの createWidgets
メソッドを下記のように変更することで行うことができます。今回は3つの操作を行えるようにしようとしているので、3つのボタンを用意しています。
class Screen:
def createWidgets(self):
#略
self.canvas.grid(column=0, row=0)
#↓これを追加
self.frame = tkinter.Frame(
self.master
)
self.frame.grid(column=0, row=1)
self.start_button = tkinter.Button(
self.frame,
text="ゲーム開始"
)
self.start_button.grid(column=0, row=0)
self.stop_button = tkinter.Button(
self.frame,
text="ゲーム停止"
)
self.stop_button.grid(column=1, row=0)
self.reset_button = tkinter.Button(
self.frame,
text="ゲームリセット"
)
self.reset_button.grid(column=3, row=0)
#↑これを追加
一旦フレームウィジェットを作成し、そのフレームウィジェット上にボタンを横に並べるように grid
メソッドを利用して配置を行なっています。フレームウィジェット上にボタンを配置するので、tkinter.Button
の第1引数はフレームウィジェットの self.frame
を指定しています。
フレームウィジェットはアプリの複雑なレイアウトの実現や部品化を行う時に便利です。下記ページで詳細を解説していますので、詳しく知りたい方は下記ページをご参照いただければと思います。
Tkinterの使い方:フレームウィジェット(Frame)の使い方ボタンに関しては、ボタン作成時(tkinter.Button()
実行時)に command
オプションでボタンが押された際に実行するメソッドや関数を指定することもできます。ただ今回は、後から Game
クラスで bind
メソッドを実行し、そこでボタンが押された時に実行するメソッドの設定を行うようにしたいと思います(イベント関連の処理は Game
クラスの役割としているため)。
スポンサーリンク
ボタンが押された時のイベント受付設定
続いてボタンが押された時のイベントを受け付けるように設定を行います。
このイベントの受付設定は bind
メソッドにより実現することができます。詳しくは下記ページをご参照いただければと思います。
このイベントの受付設定を行うために、まずは Game
クラスの __init__
を下記のように変更します。
class Game:
def __init__(self, master):
#略
for _ in range(NUM_DOG_ENEMY):
enemy = DogEnemy()
self.characters.append(enemy)
#↓これを追加
self.screen.start_button.bind("<ButtonPress>", self.start)
self.screen.stop_button.bind("<ButtonPress>", self.stop)
self.screen.reset_button.bind("<ButtonPress>", self.reset)
#↑これを追加
self.master.bind("<KeyPress-Left>", self.press)
self.master.bind("<KeyPress-Right>", self.press)
self.master.bind("<KeyPress-Up>", self.press)
self.update()
この変更により、ボタンがマウスでクリックされた時に Game
クラスの下記のメソッドが実行されるようになります。
- ゲーム開始ボタンクリック時:
start
- ゲーム停止ボタンクリック時:
stop
- ゲームリセットボタンクリック時:
reset
ただし、これらのメソッドをまだ用意していないので、とりあえず下記のように何もしないメソッドを Game
クラスに追加しておきます。
class Game:
#↓これを追加
def start(self, event):
pass
def stop(self, event):
pass
def reset(self, event):
pass
#↑これを追加
ゲーム開始ボタンの実現
次は、「ゲーム開始ボタン」がクリックされた時にゲームを開始するようにしていきたいと思います。
まず、現状なぜゲームを起動した途端(スクリプト実行直後)にゲームが開始されてしまうのかについて整理しておくと、これは下記の2つをゲーム起動時に実行しているからです(より具体的には Game
クラスの __init__
で下記を実行している)。
- ゲーム起動時にキーボードのキー入力の受付を行なっている
- なので、ゲーム起動直後からキー入力でキャラクターの移動が可能になる
- ゲーム起動時に定期的な処理を開始している
- なので、ゲーム起動直後から敵キャラクターの移動やキャラクターの画像描画等が定期的に行われる
したがって、ゲーム起動時には上記が行われないようにし、さらに「ゲーム開始ボタン」がクリックされた時に初めて上記が行われるようにしてやれば、「ゲーム開始ボタン」がクリックされた時にゲームを開始させるようにすることができます。
前者に関しては、ゲーム起動時にはキーボードのキー入力に対するイベント受付設定(bind
メソッドの実行)を行わないようにし、さらに「ゲーム開始ボタン」をクリックされた時にこの設定を行うように変更すれば良いです。
後者に関しては、まず定期的に処理が行われる仕組みを復習しておくと、これは Game
クラスの update
メソッドが実行された際に after
メソッドで再度 update
メソッドを実行するようになっているからです。これにより一度 Game
クラスの update
メソッドが実行されれば、以降定期的に繰り返し Game
クラスの update
メソッドが実行されることになっています。
ですので、Game
クラスの update
メソッドの実行をゲーム起動時に行わないようにし、さらに「ゲーム開始ボタン」がクリックされた時に初めて update
メソッドを実行するようにしてやれば、「ゲーム開始ボタン」がクリックされない限り定期的な処理が始まらないようにすることができます。
では、上記のように処理を変更していきたいと思います。
まずは Game
クラスの __init__
を下記のように変更します。これにより、ゲーム起動時にキーボードのキー入力のイベント受付設定と定期的処理の開始が行われなくなります。
class Game:
def __init__(self, master):
#略
self.screen.start_button.bind("<ButtonPress>", self.start)
self.screen.stop_button.bind("<ButtonPress>", self.stop)
self.screen.reset_button.bind("<ButtonPress>", self.reset)
#↓これを削除
#self.master.bind("<KeyPress-Left>", self.press)
#self.master.bind("<KeyPress-Right>", self.press)
#self.master.bind("<KeyPress-Up>", self.press)
#self.update()
#↑これを削除
さらに、先ほど追加した Game
クラスの start
メソッドに上記で削除した処理を追加します。具体的には、Game
クラスの start
メソッドを下記のように変更します。
class Game:
def start(self, event):
#↓これは削除
#pass
#↑これは削除
#↓これを追加
self.master.bind("<KeyPress-Left>", self.press)
self.master.bind("<KeyPress-Right>", self.press)
self.master.bind("<KeyPress-Up>", self.press)
self.update()
#↑これを追加
一応これらの変更で、ゲーム起動直後にはゲームが開始されず、「ゲーム開始ボタン」がクリックされた時にゲームが開始されるようにすることはできます。
ただ、これだけだと「ゲーム開始ボタン」がクリックされるたびに何回も Game
クラスの update
メソッドが実行されてしまって動きがおかしくなってしまいます…(二重に after
メソッドが実行されるので敵キャラクターの移動や画像描画が倍速で行われるようになる)。
ですので、ゲーム開始後は「ゲーム開始ボタン」がクリックされても何も行わないようにしていきたいと思います。
そのため、Game
クラスにゲームを開始しているかどうかを判断するためのデータ属性 is_playing
を追加し、Game
クラスの __init__
でデータ属性 is_playing
を False
に設定するようにします。
class Game:
def __init__(self, master):
self.master = master
#↓これを追加
self.is_playing = False
#↑これを追加
self.screen = Screen(self.master)
#略
さらに、Game
クラスの start
メソッドでは is_playing
が False
の場合のみ処理を行うよう、下記のように変更を行います。start
メソッドが実行されたということは、ゲーム開始されたということなので、is_playing
を True
に設定するようにもしています。
class Game:
def start(self, event):
#↓これを追加
if self.is_playing:
return
self.is_playing = True
#↑これを追加
self.master.bind("<KeyPress-Left>", self.press)
self.master.bind("<KeyPress-Right>", self.press)
self.master.bind("<KeyPress-Up>", self.press)
self.update()
これでゲーム開始後に「ゲーム開始ボタン」がクリックされても何も実行されないようにすることができます。
ゲーム停止ボタンの実現
次は「ゲーム停止ボタン」が押された時にゲームを停止するようにしていきます。
これは、先ほどの「ゲーム開始ボタン」の時とは逆に、stop
メソッドで下記の3つのことを行えば良いです。
is_playing
をFalse
に設定する- キーボードのキー入力のイベントを受け付けないように設定する
Game
クラスのupdate
メソッドの定期的実行を止める
まず、イベントの受付を取り消すためには unbind
メソッドを実行してやれば良いです。したがって、上記の前者2つを実現するためには、Game
クラスの stop
メソッドを下記のように変更すれば良いです。
class Game:
def stop(self, event):
#↓これは削除
#pass
#↑これは削除
#↓これを追加
if not self.is_playing:
return
self.is_playing = False
self.master.unbind("<KeyPress-Left>")
self.master.unbind("<KeyPress-Right>")
self.master.unbind("<KeyPress-Up>")
#↑これを追加
念の為ゲーム開始していない状態で押された場合には何も処理を行わないように最初の if
文を入れています。
また、Game
の update
メソッドの定期的実行を止めるためには、まず after
メソッドの実行を after_cancel
メソッドによりキャンセルする方法が考えられます。
もしくは、stop
メソッドの中で is_playing
を False
に設定するのですから、Game
クラスの update
メソッドの中で is_playing
が False
の場合に after
メソッドを実行しないようにすることでも定期的実行を止めることもできます。
今回は後者の方法を選択したいと思います。具体的には、下記のように Game
クラスの update
メソッドの先頭部分を変更し、is_playing
が False
の場合は何も処理を行わないようにします(is_playing
は stop
メソッドで False
に設定される)。
class Game:
def update(self):
#↓これを追加
if not self.is_playing:
return
#↑これを追加
if self.player.state == Character.STATE_NORMAL:
self.master.after(UPDATE_TIME, self.update)
#略
このように変更を行えば、「ゲーム停止ボタン」がクリックされた時にゲームが停止することが確認できると思います。さらに、ゲーム停止した状態で「ゲーム開始ボタン」がクリックされればゲームが再開できることも確認できると思います。
ちなみに、今回は使用しませんでしたが、after_cancel
メソッドも after
メソッドによる定期的処理を停止させる時に便利です。下記ページの途中で after_cancel
についても解説していますので、興味のある方はぜひ読んでみてください。
スポンサーリンク
ゲームリセットボタンの実現
この章の最後として、「ゲームリセットボタン」が押された際にゲームをリセット、つまり最初からやり直せるようにしていきたいと思います。
ゲームのリセットに関しては、Screen
クラスや Character
クラスのオブジェクトを初期化してやることで実現することができます。これらの初期化はオブジェクト生成時に行なっていますので、要は各オブジェクトを生成し直してやればゲームのリセットが実現できます。
これらのオブジェクトの生成は Game
クラスの __init__
から行なっていますので、「ゲームリセットボタン」が押された際にこれを実行してやれば良いのですが、Game
クラスから自身の __init__
を実行するのはちょっと違和感があります。
そのため、オブジェクト生成を行う部分を __init__
から新たなメソッド prepare
に移動させ、「ゲームリセットボタン」がクリックされた際に、その prepare
をメソッドを実行するようにしたいと思います(つまり reset
メソッドから prepare
メソッドを実行する)。
このために、まずは下記のように Game
クラスの __init__
の変更と prepare
メソッドの追加を行います。__init__
には self.master = master
と self.is_playing = False
と self.prepare()
のみが残ることになります。
class Game:
def __init__(self, master):
self.master = master
self.is_playing = False
#↓これは削除してprepareに移動
#self.screen = Screen(self.master)
#略
#self.screen.reset_button.bind("<ButtonPress>", self.reset)
#↑これは削除してprepareに移動
#↓これを追加
self.prepare()
#↑これを追加
#↓これを追加
def prepare(self):
self.screen = Screen(self.master)
self.characters = []
self.player = Player()
self.characters.append(self.player)
goal = Goal()
self.characters.append(goal)
for _ in range(NUM_CAT_ENEMY):
enemy = CatEnemy()
self.characters.append(enemy)
for _ in range(NUM_DOG_ENEMY):
enemy = DogEnemy()
self.characters.append(enemy)
self.screen.start_button.bind("<ButtonPress>", self.start)
self.screen.stop_button.bind("<ButtonPress>", self.stop)
self.screen.reset_button.bind("<ButtonPress>", self.reset)
#↑これを追加
ボタンに対する bind
実行部分も prepare
側に移動させたのは、この bind
を実行するオブジェクトであるボタンウィジェットが prepare
側の Screen()
の中で行われるからです(Screen()
が実行されるたびに bind
も実行しなくてはならない)。
さらに、Game
クラスの reset
メソッドを下記のように変更します。
class Game:
def reset(self, event):
#↓これは削除
#pass
#↑これは削除
#↓これを追加
self.stop(None)
self.screen.start_button.unbind("<ButtonPress>")
self.screen.stop_button.unbind("<ButtonPress>")
self.screen.reset_button.unbind("<ButtonPress>")
self.prepare()
#↑これを追加
stop
メソッドでゲームを停止してから、prepare
メソッドでのオブジェクト生成のやり直しを行うようにしています。
また、一応ボタンに対するイベント設定も unbind
で解除してから prepare
メソッドを実行するようにしています。prepare
メソッドの中でガベージコレクションが動作して古いボタンのオブジェクトも削除されるはずなので、この unbind
はもしかしたら不要かもしれません。ちょっと確証が持てなかったため、念の為実行するようにしています。
これらの変更により、「ゲームリセットボタン」をクリックしてから「ゲーム開始ボタン」をクリックすれば、ゲームを最初からやり直すことができるようになります。
例えばゲームクリアやゲームオーバーになってからでもゲームのやり直しが可能です。ただ、敵キャラクターの位置は現状ランダムに決めているため、敵キャラクターの初期位置まで元に戻るわけではないので気をつけてください。
新たなキャラクターを作成する
次は新たなキャラクターを作る例を紹介していきます。
新たな敵キャラクターを作成する
まずは新たな敵キャラクターを作成する例を紹介します。
敵キャラクターを新たに作成するためには、基本下記の3つを行えば良いです。
- 新たな敵キャラクター用の
Enemy
クラスのサブクラスを作成する - 作成したサブクラスのオブジェクトを生成し、
Game
クラスのデータ属性characters
に追加する - 必要に応じて
collided
メソッドで新たな敵キャラクター用の衝突時の処理を追記する
ここでは無敵な敵キャラクターを TurtleEnemy
クラスとして作成していく例で説明していきます。
ちなみにですが、私が動作確認時に使用した画像は いらすとや の下記 URL の亀の画像になります。
https://www.irasutoya.com/2017/03/blog-post_31.html
下の画像は上記 URL のものを転載させていただいたものになります。
転載元:いらすとや
サブクラスの作成
まずは TurtleEnemy
クラスを作成していきます。といっても作り方は他の敵キャラクターの時と同じです。
この辺りの敵キャラクターの作成方法は下記ページで解説していますので、詳しく知りたい方はこちらを読んでみてください。
【Python/tkinter】横スクロールアクションゲームを作る(敵キャラクターの作成)今回は下記のような TurtleEnemy
クラスを作成したいと思います。"画像のファイルパス"
部分は亀の画像のファイルパスが指定されることを想定しています。
class TurtleEnemy(Enemy):
def __init__(self):
super().__init__("画像のファイルパス", (80, 80), False)
self.jump_height = 0
self.speed_x = 10
jump_height
を 0
にしているのでジャンプできないようになっています。また speed_x
を 10
に設定しているので他のキャラクターよりも動作が遅くなっています。この辺りのサブクラスごとのカスタマイズは、作成したいキャラクターに応じて適当に変えてやれば良いです。
オブジェクトの生成
次は先程作成したオブジェクトを生成し、Game
クラスのデータ属性 characters
に追加します。この characters
にオブジェクトを格納しておけば、自動的に定期的に移動したり画像が描画されたりするようになります。
class Game:
def __init__(self, master):
#略
self.characters = []
self.player = Player()
self.characters.append(self.player)
goal = Goal()
self.characters.append(goal)
for _ in range(NUM_CAT_ENEMY):
enemy = CatEnemy()
self.characters.append(enemy)
for _ in range(NUM_DOG_ENEMY):
enemy = DogEnemy()
self.characters.append(enemy)
#↓これを追加
enemy = TurtleEnemy()
self.characters.append(enemy)
#↑これを追加
上記では __init__
を変更していますが、ボタンで「ゲーム開始」「停止」「リセット」を行う のカスタマイズをされている場合は prepare
メソッドを変更する必要があるので注意してください。
以上の変更で新たに作成した敵キャラクターが画面に表示され、定期的な移動や当たり判定が他の敵キャラクター同様に行われるようになります。
当たった時の処理
もし当たった時の処理を他の敵キャラクターと変更したい場合は Game
クラスの collide
メソッドを変更する必要があります。
このメソッドでクラスごとに当たった時の処理を定義していますので、作成したクラスに対して新たな処理を追加してやれば、当たった時にその処理が実行されるようになります。
例えば下記のように collide
メソッドを変更すれば、TurtleEnemy
のオブジェクトと Player
のオブジェクトが当たった際には、必ず Player
が倒されてゲームオーバーになるようになります。これで無敵な敵キャラクターの完成です。
class Game:
def collide(self, character, opponent):
#略
elif isinstance(character, Goal) and isinstance(opponent, Enemy):
if opponent.direction == Character.DIRECTION_LEFT:
opponent.move(Character.DIRECTION_RIGHT)
else:
opponent.move(Character.DIRECTION_LEFT)
#↓これを追加(Enemyに対する処理よりも上に追加)
elif isinstance(character, Player) and isinstance(opponent, TurtleEnemy):
character.defeated()
elif isinstance(character, TurtleEnemy) and isinstance(opponent, Player):
opponent.defeated()
#↑これを追加(Enemyに対する処理よりも上に追加)
elif isinstance(character, Player) and isinstance(opponent, Enemy):
if character.isTrampling(opponent):
opponent.defeated()
character.trample()
else:
character.defeated()
elif isinstance(character, Enemy) and isinstance(opponent, Player):
if opponent.isTrampling(character):
character.defeated()
opponent.trample()
else:
opponent.defeated()
変更時のポイントは、スーパークラスである Enemy
に対する elif
文よりも上側にサブクラスの当たった時の処理を追加する必要がある点です。
isinstance
は第1引数のオブジェクトが第2引数のクラスのインスタンスであるかどうかを判断する関数ですが、第2引数にスーパークラスのものを指定した場合、そのサブクラスのインスタンスであっても True
が返却されます。
ですので、スーパークラスである Enemy
に対する elif
文よりも下側に追加してしまうと、character
や opponent
が追加したサブクラスのオブジェクトであっても isinstance(character, Enemy)
or isinstance(opponent, Enemy)
が True
を返却し、Enemy
側に対する処理が実行されてしまいます。
こういう処理の順序も意識する必要があるので、collide
メソッドは Character
の各サブクラス側で実装した方が良かったかもなぁとちょっと思ってます…。
とりあえず、新たな敵キャラクターの追加の仕方のイメージは伝わったのではないでしょうか?
スポンサーリンク
アイテムを作成する
ここまで解説してきた枠組みに捉われず、他の種類のキャラクターを追加するのも良いカスタマイズだと思います。
ということで、次はアイテムを作成する例を紹介します(アイテムがキャラクターなのかは置いといて…)。
アイテムも基本的に敵キャラクターを作成する時と同様で、まず下記の3つが必要になります(スーパークラスは Character
にしていますが、追加したいアイテムが多いのであれば Item
クラスのようなスーパークラスを作成しても良いと思います)。
- 新たなアイテム用の
Character
クラスのサブクラスを作成する - 作成したサブクラスのオブジェクトを生成し、
Game
クラスのデータ属性characters
に追加する collided
メソッドで新たなアイテム用の衝突時の処理を追記する
今回は取得すれば(当たったら)無敵状態になれるアイテム用のクラス Star
を作っていきたいと思います。
無敵状態なのですから、通常状態とは異なる動作を実現する必要があります。今回は無敵状態の間は敵に踏みつけられても倒されないようにしていきたいと思います。
そのため下記の処理も追加で実現していく必要があります
- 当たった時に操作キャラクターを無敵状態にする
- 無敵状態になったら敵に踏みつけられても倒されないようにする
- 時間が経ったら無敵状態から通常状態に戻るようにする
- 無敵状態であることが分かるように、無敵状態の時は見た目を変更する
ここからは変更箇所だけサラッと紹介していきたいと思います。
サブクラスの作成
今回は下記のような Star
クラスを作成しました。"画像のファイルパス"
部分は星の画像のファイルパスが指定されることを想定しています。
class Star(Character):
def __init__(self):
super().__init__("画像のファイルパス", (50, 50))
self.x = random.randrange(300, GAME_WIDTH - 300)
self.direction = Character.DIRECTION_LEFT
self.speed_x = 30
self.speed_y = 30
self.jump_height = 50
def update(self):
self.move(self.direction)
self.move(Character.DIRECTION_UP)
if self.x == 0:
self.direction = Character.DIRECTION_RIGHT
elif self.x == GAME_WIDTH - self.width:
self.direction = Character.DIRECTION_LEFT
super().update()
ちなみにですが、私が動作確認時に使用した画像は いらすとや の下記 URL の星の画像になります。
https://www.irasutoya.com/2013/08/blog-post_5160.html
下の画像は上記 URL のものを転載させていただいたものになります。
転載元:いらすとや
オブジェクトの生成
オブジェクトの生成はコンストラクタの実行により行い、いつも通り生成したオブジェクトは Game
クラスの characters
に追加します。
class Game:
def __init__(self, master):
#略
enemy = TurtleEnemy()
self.characters.append(enemy)
#↓これを追加
star = Star()
self.characters.append(star)
#↑これを追加
#略
上記では __init__
を変更していますが、ボタンで「ゲーム開始」「停止」「リセット」を行う のカスタマイズをされている場合は prepare
メソッドを変更する必要があるので注意してください。
当たった時の処理
当たった時の処理も下記のように collide
メソッドの最後に追加しています。
class Game:
def collide(self, character, opponent):
#略
#↓これを追加
elif isinstance(character, Star) and isinstance(opponent, Player):
opponent.muteki()
character.defeated()
elif isinstance(character, Player) and isinstance(opponent, Star):
character.muteki()
opponent.defeated()
#↑これを追加
muteki
は後から追加するキャラクターを無敵状態にするメソッドになります。
当たった時にアイテム側は defeated
メソッドを実行していますが、このメソッドを実行している理由は当たったアイテムを非表示にするためです。
今回は簡単のためこのメソッドを使用していますが、別にアイテムは倒されるわけではないので、本当は別途別のメソッドを用意した方が良いです。
無敵状態への遷移
次は先程追加した当たった時の処理で実行される muteki
メソッドを作成します。他の状態と見分けがつくように、クラス変数の追加も行っています。
class Character:
STATE_NORMAL = 0
STATE_CLEAR = 1
STATE_DEFEATED = 2
#↓これを追加
STATE_MUTEKI = 3
#↑これを追加
class Player(Character):
#↓これを追加
def muteki(self):
self.state = Character.STATE_MUTEKI
self.muteki_time = 5000
#↑これを追加
muteki_time
は無敵状態でいる間の時間です。単位は ms になります。後でこのデータ属性を使用し、ここで指定した時間が経過した後に通常状態に戻るようにしていきます。
ただ、上記で state
が Character.STATE_NORMAL
から変化してしまうと、Game
クラスの update
メソッドが定期実行されなくなってしまうため、この update
メソッドの下記の変更も必要になります。
class Game:
def update(self):
#略
if self.player.state == Character.STATE_NORMAL or self.player.state == Character.STATE_MUTEKI: #←ここを変更
self.master.after(UPDATE_TIME, self.update)
#略
倒されないようにするための処理
続いて、Player
クラスでスーパークラスの defeated
メソッドをオーバーライドし、無敵状態の場合は倒されないようにします。
class Player(Character):
#↓これを追加
def defeated(self):
if self.state != Character.STATE_MUTEKI:
super().defeated()
#↑これを追加
時間経過で通常状態に戻す処理
以上の変更でスタートと操作キャラクターが当たった後は、操作キャラクターが敵キャラクターに当たっても倒されないようになりました。
ただ、ずっと無敵状態というのもゲームとして面白くないので、時間経過によって通常状態に戻るようにするため、Player
クラスでスーパークラスの update
メソッドを下記のようにオーバーライドしたいと思います。
class Player(Character):
#↓これを追加
def update(self):
if self.state == Character.STATE_MUTEKI:
self.muteki_time -= UPDATE_TIME
if self.muteki_time <= 0:
self.state = Character.STATE_NORMAL
super().update()
#↑これを追加
この変更で、muteki
メソッドで設定された muteki_time
が 0
以下になったら通常状態に戻るようになります。
この update
メソッドは after
メソッドにより UPDATE_TIME
ごとに実行されるので、実行されるたびに muteki_time
を UPDATE_TIME
毎に減らしていけば、大体 muteki
メソッドで設定された muteki_time
の時間が経過した後に通常状態に戻るようにすることができます。
ただしこの方法だと誤差が出るので、通常状態に戻るまでの時間を正確にしたいのであれば、時間を計測し、その計測時間から通常状態に戻るかどうかを判断するようにしたほうが良いです。
無敵状態用の画像を表示する
最後に無敵状態であることが視覚的に分かるように、無敵状態の時は通常状態の時から操作キャラクターの見た目を変化させるようにしていきたいと思います。
まずは無敵状態用の画像を作成するために、下記のように変更を行います。
class Character:
def prepareImage(self, path, size, is_right=True):
#略
if len(mirrored_channels) == 4:
mirrored_alpha = mirrored_channels[3]
else:
mirrored_alpha = Image.new("L", (mirrored_image.width, mirrored_image.height), 255)
#↓これを追加
muteki_image = ImageOps.invert(resized_image.convert("RGB"))
muteki_image.putalpha(resized_alpha)
mirrored_muteki_image = ImageOps.mirror(muteki_image)
#↑これを追加
if is_right:
self.right_image = ImageTk.PhotoImage(resized_image)
self.left_image = ImageTk.PhotoImage(mirrored_image)
#↓これを追加
self.muteki_right_image = ImageTk.PhotoImage(muteki_image)
self.muteki_left_image = ImageTk.PhotoImage(mirrored_muteki_image)
#↑これを追加
self.right_alpha = resized_alpha.getdata()
self.left_alpha = mirrored_alpha.getdata()
else:
self.left_image = ImageTk.PhotoImage(resized_image)
self.right_image = ImageTk.PhotoImage(mirrored_image)
#↓これを追加
self.muteki_left_image = ImageTk.PhotoImage(muteki_image)
self.muteki_right_image = ImageTk.PhotoImage(mirrored_muteki_image)
#↑これを追加
self.left_alpha = resized_alpha.getdata()
self.right_alpha = mirrored_alpha.getdata()
#略
無敵状態用の画像として、通常状態用の画像の色を反転したものを作成しています。とりあえず今回は通常状態から見た目を変化させることだけを目的とし、簡単に実行できる色の反転で済まさせていただいてます。
色の反転は、PIL の ImageOps.invert
により行うことができます。
ただアルファチャンネルが設定されていると上手く反転できないようなので、一旦 convert
メソッドでアルファチャンネル無しの画像に変換してから ImageOps.invert
を実行し、さらにその後に putalpha
で通常状態の時と同様にアルファチャンネルの設定を行うようにしています。
最後に、画像を取得するメソッドで、キャラクターの状態によって返却する画像オブジェクトを変更するようにすれば完成です。
class Character:
def getImage(self):
#↓これを追加
if self.state == Character.STATE_MUTEKI:
if self.direction == Character.DIRECTION_RIGHT:
return self.muteki_right_image
elif self.direction == Character.DIRECTION_LEFT:
return self.muteki_left_image
else:
if self.direction == Character.DIRECTION_RIGHT:
return self.right_image
elif self.direction == Character.DIRECTION_LEFT:
return self.left_image
#↑これを追加
#↓これは削除
#if self.direction == Character.DIRECTION_RIGHT:
# return self.right_image
#elif self.direction == Character.DIRECTION_LEFT:
# return self.left_image
#↑これは削除
以上の変更を行なったスクリプトを実行すれば、ゲーム画面上にアイテムが登場し、そのアイテムに当たった後には操作キャラクターの見た目が変化し、無敵状態(他の敵キャラクターに当たっても倒されない)になることが確認できると思います(約5秒後に通常状態に戻る)。
また、getImage
メソッドを下記のように変更した場合、無敵状態の際に通常状態用の画像と無敵状態用の画像が交互に描画されるようになります(ちょっとマリオでスターを取った時の見た目にも近づく)。
ただし、下記では muteki_time
の値が UPDATE_TIME
で割り切れることを前提とした作りになっている点に注意してください。
class Character:
def getImage(self):
if self.state == Character.STATE_MUTEKI:
if self.muteki_time % (UPDATE_TIME * 2) == 0:
if self.direction == Character.DIRECTION_RIGHT:
return self.muteki_right_image
elif self.direction == Character.DIRECTION_LEFT:
return self.muteki_left_image
else:
if self.direction == Character.DIRECTION_RIGHT:
return self.right_image
elif self.direction == Character.DIRECTION_LEFT:
return self.left_image
else:
if self.direction == Character.DIRECTION_RIGHT:
return self.right_image
elif self.direction == Character.DIRECTION_LEFT:
return self.left_image
こんな感じで、キャラクターの状態を追加することで、いろんなバリエーションのゲームにカスタマイズすることができます。
応用すれば、踏みつけられた敵キャラクターを一定時間点滅させた後に非表示にするようなことも可能です。
ただキャラクターの状態が多くなるとどんどんスクリプトが複雑になっていくので、メソッドの分割はもちろんのこと、キャラクターの状態毎にクラスを用意することを検討した方が良いと思います。
まとめ
このページでは、ここまで作成してきた横スクロールアクションゲームの「カスタマイズ例」を紹介しました。
「横スクロールアクションゲームの作り方」の解説ページとしては、このページが最後となります。ここまで読んでくださった方、本当にありがとうございます。
少しでも新しく学べたことがあったのであれば幸いですし、プログラミングの楽しさが少しでも伝わったのであれば嬉しいです。
今回ゲームを作成していく上で解説した内容については、他のゲームやアプリを開発する上でも役立つことが多いと思いますので、是非このゲームの作り方を応用して、いろんなアプリやゲーム作りに挑戦してみてください!
オススメ参考書(PR)
簡単なアプリやゲームを作りながら Python について学びたいという方には、下記の Pythonでつくる ゲーム開発 入門講座 がオススメです!ちなみに私が Python を始めるときに最初に買った書籍です!
下記ようなゲームを作成しながら Python の基本が楽しく学べます!素材もダウンロードして利用できるため、作成したゲームの見た目にも満足できると思います。
- すごろく
- おみくじ
- 迷路ゲーム
- 落ち物パズル
- RPG
また本書籍は下記のような構成になっているため、Python 初心者でも内容を理解しやすいです。
- プログラミング・Python の基礎から解説
- 絵を用いた解説が豊富
- ライブラリの使い方から解説(tkitner と Pygame)
- ソースコードの1行1行に注釈
ゲーム開発は楽しくプログラミングを学べるだけでなく、ゲームで学んだことは他の分野のプログラミングにも活かせるものが多いですし(キーボードの入力受付のイベントや定期的な処理・画像や座標を扱い方等)、逆に他の分野のプログラミングで学んだ知識を活かしやすいことも特徴だと思います(例えばコンピュータの動作に機械学習を取り入れるなど)。
プログラミングを学ぶのにゲーム開発は相性抜群だと思います。
Python の基礎や tkinter・Pygame の使い方をご存知なのであれば、下記の 実践編 をいきなり読むのもアリです。
実践編 では「シューティングゲーム」や「アクションゲーム」「3D カーレース」等のより難易度の高いゲームを作りながらプログラミングの力をつけていくことができます!
また、単にゲームを作るのではなく、対戦相手となるコンピュータの動作のアルゴリズムにも興味のある方は下記の「Pythonで作って学べるゲームのアルゴリズム入門」がオススメです。
この本はゲームのコンピュータ(AI)の動作アルゴリズム(思考ルーチン)に対する入門解説本になります。例えばオセロゲームにおけるコンピュータが、どのような思考によって石を置く場所を決めているか等の基本的な知識を得ることが出来ます。
プログラミングを挫折せずに続けていくためには楽しさを味わいながら学習することが大事ですので、特にゲームに興味のある方は、この辺りの参考書と一緒に Python を学んでいくのがオススメです!