皆さんこんにちは。普段何気なく使っているものでも、作る側にまわるとこんなにも大変なことをしているのだなぁとしみじみ感じてしまう小幡です。
古本は丁寧に掃除や修復が行われていたり、コンビニの食べ物は一般的な調理とは全く別の様々な工程を経ていたり、蛇口を捻ったら水が出てくるということですら、大変な苦労のおかげで実現できている技術だと思うと、日々何気ないところにも感謝しながら生活できることに気が付きました。
ゲームもそのうちの1つで、コントローラーやディスプレイ、ソフトウェアやハードウェアも遊べて当たり前という感覚でしたが、作ったりする側になるとそのコントローラー1つを作ろうとしても膨大なコストがかかる事に気が付くことができました。
そんなコントローラー的な処理、今回は「プレイやーを移動させる」処理を追加していきます。
前回の記事は以下リンクからどうぞ
前回のおさらい
- 列挙型とは何かを知る
- 列挙型のクラスを作る
- 列挙型を使いフィールドマップを見やすくする
ということで、前回までに第1回で紹介したフィールドマップが完成しました。
プレイヤーを移動させるとは?
様々な2Dゲームが存在する昨今、プレイヤーを移動させるだけでも様々な方法があることは、皆さんも容易に想像つくかと思います。
Unityなどのゲームエンジンを使う事で簡単に「プレイヤーの移動」に関数実装は可能だと思いますが、もちろん今回は使えませんし、標準出力だけでプレイヤーを移動させるという制限もついています。しかしながら、この制限があるからこそ、プレイヤーの移動は簡単に実装できるのではないだろうか?と考えています。
というのも、本格的なゲームを作ろうとすると、この「プレイヤーの移動」は遊んでいる側からは想像もつかないプログラムが組まれている可能性が高いです。
参考までにセガさんが一般公開している教材のリンクを貼っておきます。(たまたま目に入っただけで深い意味はありません。)こちらの記事では3Dで使うような技術についての説明が行われているようです。
残念ながら私にはまったく理解できない内容となっています。線形代数の基礎すらまともに理解できていませんので、少なくとも最初の方は理解できるようになりたいところではありますが…。
さて、前置きが長くなりましたが、「プレイヤーを移動させる」処理は、なかなか大変なものになります。気合を入れて作業していきましょう!
実際に実装する方法は、「プレイヤーの持つ位置座標を入力されたキーに応じて増減させる」という方法です。
これは、
- プレイヤーに位置座標を持たせる
- キー入力を受け付ける
- キーの値を位置座標に反映させる
- 最後にフィールドマップを再出力する
という4ステップを踏めば、見事に「プレイヤーの移動」処理が完成します。それでは1つずつ見ていきましょう。
1,プレイヤーに位置情報を持たせる
これは既にマップクラスを作成しているので、このクラスのメンバに「位置情報」を持たせてあげれば上手く行きそうですね。そして、前回はマップクラスのコンストラクタは作成しませんでしたが、一気にマップ情報を持たせてしまいましょう。
前回までのmap.pyの全体は以下です。
from enum import Enum
class Map:
map_lists = [
['B', 'B', 'B', 'B', 'B', 'B', 'B', 'B', 'B', 'B', 'B', 'B', 'B', 'B', 'B', 'B', 'B', 'B', 'B', 'B', 'B', 'B', 'B', 'B'],
['B', 'P', 'E', 'B', 'E', 'E', 'E', 'E', 'E', 'E', 'E', 'E', 'E', 'E', 'H', 'E', 'E', 'E', 'E', 'E', 'E', 'E', 'H', 'B'],
['B', 'E', 'E', 'B', 'E', 'E', 'E', 'E', 'E', 'B', 'E', 'E', 'E', 'E', 'E', 'E', 'E', 'E', 'E', 'E', 'E', 'E', 'E', 'B'],
['B', 'H', 'E', 'B', 'E', 'E', 'B', 'E', 'E', 'B', 'E', 'E', 'B', 'E', 'E', 'B', 'E', 'E', 'E', 'E', 'E', 'E', 'E', 'B'],
['B', 'H', 'E', 'B', 'E', 'E', 'B', 'E', 'E', 'B', 'H', 'E', 'B', 'E', 'E', 'B', 'E', 'E', 'E', 'E', 'E', 'E', 'E', 'B'],
['B', 'H', 'E', 'E', 'E', 'E', 'B', 'E', 'E', 'B', 'E', 'E', 'B', 'E', 'E', 'B', 'E', 'E', 'E', 'E', 'E', 'E', 'E', 'B'],
['B', 'E', 'E', 'B', 'E', 'E', 'B', 'E', 'E', 'B', 'E', 'E', 'B', 'E', 'E', 'B', 'E', 'E', 'E', 'E', 'E', 'E', 'E', 'B'],
['B', 'E', 'E', 'E', 'E', 'E', 'B', 'E', 'E', 'B', 'E', 'E', 'B', 'E', 'E', 'B', 'E', 'E', 'E', 'E', 'E', 'E', 'E', 'B'],
['B', 'E', 'S', 'B', 'E', 'E', 'B', 'E', 'E', 'B', 'E', 'E', 'B', 'E', 'E', 'B', 'E', 'E', 'E', 'E', 'E', 'E', 'E', 'B'],
['B', 'E', 'E', 'E', 'E', 'E', 'B', 'E', 'E', 'E', 'E', 'E', 'B', 'E', 'E', 'E', 'E', 'E', 'E', 'E', 'E', 'E', 'E', 'B'],
['B', 'E', 'E', 'E', 'E', 'H', 'B', 'W', 'E', 'E', 'E', 'E', 'B', 'E', 'H', 'E', 'E', 'E', 'E', 'E', 'E', 'E', 'G', 'B'],
['B', 'B', 'B', 'B', 'B', 'B', 'B', 'B', 'B', 'B', 'B', 'B', 'B', 'B', 'B', 'B', 'B', 'B', 'B', 'B', 'B', 'B', 'B', 'B'],
]
def show(self):
"""マップを表示
"""
# 二次元配列の文字列を、コマンドライン表示用に変換
for array in self.map_lists:
map = ''
for string in array:
map = map + MapItem(string).map_item
print(map)
class MapItem(str, Enum):
"""マップアイテム一覧
"""
def __new__(cls, value, map_item, title, description):
obj = str.__new__(cls, value)
obj._value_ = value
obj.map_item = map_item
obj.title = title
obj.description = description
return obj
BLOCK = 'B', '#', '壁', '侵入不可エリア(壁)'
EMPTY = 'E', ' ', '空地', '何もないフィールド'
PLAYER = 'P', 'P ', 'プレイヤー', 'プレイヤーの現在位置'
GOAL = 'G', 'G ', 'ゴール', 'ゴールの位置'
WEAPON = 'W', '剣', '勇者の剣', '武器/勇者の剣/持っているだけで攻撃力アップ:'
SIELD = 'S', '盾', '勇者の盾', '防具/勇者の盾/持っているだけで防御力アップ'
HERBS = 'H', '薬', '薬草', '道具/薬草/使うとHPがすこし回復する'
マップクラスにはmap_listsという2次元配列と、showメソッドというマップを表示させる関数が入っている状態です。
ここにコンストラクタを作成します。場所はmap_listsとshowメソッドの間にしておきます。
def __init__(self):
self.now_h = 1
self.now_w = 1
self.counter = 0
self.goal_flg = False
self.game_over_flg = False
self.show_item_flg = False
self.field = ''
プレイヤークラス作成の時にも使いましたが、”__init__”を使うことで、コンストラクタとなります。そしてこの中ではいくつかの値を付与しています。順番に見ていきましょう
selfというのは、クラスメソッドの紹介の時などにも説明しましたが、インスタンス生成された時の自分自身を指します。これはmain関数の中で”map = Map()”とインスタンスが作成された時にselfがmapに置き換り、そのメンバを呼び出したい時は”map.now_h”というようにしてい使用します。
そのメンバは以下のように定義しています。
now_h | 現在座標の行 |
now_w | 現在座標の列 |
counter | カウンター(モンスターとのエンカウントで使用) |
goal_flg | ゴールしたかどうかの判定で使用 |
game_over_flg | ゲームオーバーかどうかの判定で使用 |
show_item_flg | アイテム表示画面へ遷移するかどうかの判定で使用 |
field | フィールドの値 |
今は「たくさんのメンバが用意された」程度の認識で大丈夫です。後々使います。
一番重要なのは、現在座標の行と列です。これらにそれぞれ1を与えています。
なぜ1を与えているかというと、2次元配列で用意したマップの最初のプレイヤーの立ち位置が”[1, 1]”だからです。二次元配列の説明でも詳しく説明しましたが、配列のインデックスは0から始まりますので”[1, 1]”というのは、フィールドマップを標準出力した画面から見ると「上から2行目、左から2列目」という位置座標を現在位置としていることになります。
すこしややこしい話ですが、このインデックスが0から始まるというのは慣れるしかありません。
こうすることで、”map_lists”の初期のプレイヤー位置とマップのメンバが持つプレイヤーの位置座標を一致させることができました。この設定は少し複雑に感じると思いますが、後程この座標をずらすことでプレイヤーの”P”をずらすことが可能になっていきます。
先に簡単に説明しておくと、現在位置はインデックス[1, 1]になっているので、1つ下の座標のインデックスは[2, 1]です。インデックス[行, 列]と書くとわかりやすいでしょうか?さらに1つ下に進と”薬”が表示してある場所であるインデックス[3, 1]に到達できそうですね。
ということは「下に移動するキー」が入力された場合、元の位置インデックス[1, 1]の”P”を空白フィールド” ”に書き換えて、進んだ先のインデックス[2, 1]に”P”を書き換えて表示できれば移動したように見えるかもしれません。
2,キー入力を受け付ける
さて、”P”の位置をインデクス[1, 1]として現在位置を表現することができるようになりました。これを「下に移動したい」と考える時に、コントローラーからの入力を受け付ける必要がありますね。
しかしながら、今回は標準入力と標準出力しか扱えませんので、当然ゲーム機のような十字キーは使えません。ここではよくPCゲームの移動入力キーとして使われる「W・A・S・D」の4つのキーを「上・左・下・右」と置き換えて使用します。
なぜ「W・A・S・D」を使うのか全く理解できない人は、一度キーボードの「W・A・S・D」キーの位置を確認してみてください。これらのキーは左手のホームポジションで3つをカバーしつつ上移動を1つ上のWキーで使う事ができるという直感的な配置になっています。
直感的と言っても、ゲーム機のコントローラーや、スマホのタッチパネル操作に比べると非常に扱いずらいかもしれませんが、PCゲームに慣れている人にはよくあるキー配置だと思います。先に説明しておくと、この「W・A・S・D」キーに加えてQキーではゲーム終了、Eキーでは取得したアイテム一覧表示などの機能を実装する予定です。
それでもまだよくわからないという人は、第1回に配置してあります完成予想のゲームをダウンロードして1度プレイしてみる事をお薦めします。どのようにプレイヤーが移動したりするのかを実際に体験してみてからのほうが、コードを書いているときにも、「今はこの処理を作っているんだ」とより基礎の理解が進むと思います。こちらにも配置しておきますので、是非一度プレイしてみてください。
遊び方ですが、ZIPを解凍してから、コマンドラインで解答したフォルダの中に入り、”python ./main.py”で実行してください。
それでは実際にプレイヤーからのキー入力を受け付ける処理を書いていきますが、前回までのmain.pyは以下の状態になっています。
from map import Map
from player import Player
def main():
# タイトルを表示
input('=== Game Start! ===')
# プレイヤー名の入力
print('プレイヤー名を入力してください。')
player_name = input()
player = Player(player_name)
print('ようこそ、{}さん!'.format(player.name ))
# マップを表示
map = Map()
map.show()
# マップを移動
input('プレイヤーはマップを移動した!')
# モンスターとバトル
input('スライムがあらわれた!')
# バトルに勝利
input('スライムをたおした!')
# バトルに勝利
input('=== Game Clear! ===')
if __name__ == '__main__':
main()
15行目から20行目までがマップの処理となっていますので、こちらをピックアップします。
# マップを表示
map = Map()
map.show()
# マップを移動
input('プレイヤーはマップを移動した!')
2行目はマップクラスからマップのインスタンスを生成していますので、1度だけ処理すれば2回目の必要はないです。
しかし、3行目から6行目までは、プレイヤーがキーを入力するたびに処理が必要になります。これは「下に移動」したら「マップを表示」を繰り返せば実現できそうです。
では、今回の場合の「繰り返し処理」はどのように記述すれば良いでしょうか?
Whileループを使う
様々な方法が思い浮かぶかもしれませんが、今回はWhile文を使用します。
# マップのインスタンス生成
map = Map()
# ループ処理
while True:
# キー入力受付
key = input('キーを入力してください')
# マップを表示
map.show()
# マップを移動
print(f'{key}を受け付けました')
# Qキーの場合、ループ終了
if key == 'q':
break
While文は条件式がTrueの場合、ずっとループを繰り返すループ処理です。
今回の条件式には”True”が直接入っていますので、式の答えは、ずっとそのままの”True”です。なので、特に何も起こらなければ、無限ループになります。しかし、コード末を見て貰えればわかると思いますが”break”があるので、この”break”に到達したときに、このループから抜ける事ができるという構造になっています。
では上から見ていきます。キー入力受け付きの箇所では、何度も出てきた”input”関数を使ってキー入力を受け付けており、受け取ったもものを文字列として変数keyに入れています。
その後マップの表示を行い、”print(f'{key}を受け付けました’)”によって、変数keyの値を標準出力しています。これも以前の説明でお伝えした通り、format関数ではなく、”f”を使うことで、文字列への変数の挿入を実現しています。
最後のif文ですが、これは変数keyの値が文字列qだった場合にbreakしてループから抜けるという処理です。つまり他のキーが入力され続けている間は、ひたすら入力されたキーを標準出力で返し続け、もしもqキーだった場合にループを抜けます。
ちなみに、今回は半角英小文字のqである場合の条件式にしていますが、もう少しユーザー目線の実装をするのであれば、大文字のQだったり全角のQなども対象にすべきかもしれません。つまり同じqであっても、今のままだと全角のqが入力されていればループから抜けられないという状態なのでユーザーに優しいとは言い難い状況です。この修正は後程行う予定です。
さて、このWhile文とinput関数を使う事でコントローラーのように入力を受け付けることができるようになりました。ループ処理にしたことで4つ目のステップのマップを再出力するも同時にできるようになりました。
後は、受け取ったキーの値を、先ほど作成した現在位置座標に反映させることができれば「プレイヤーの移動」が完成しそうです。
3,キーの値を位置座標に反映させる
まずは少しコードが長くなっても良いという前提で、素直に与えられた入力に従ってプレイヤーを移動させてみます。
まず、マップの移動はマップのインスタンスに任せることにします。”map.move(key)”に修正しています。
# ループ処理
while True:
# マップを表示
map.show()
# キー入力受付
key = input('キーを入力してください')
# マップ移動
map.move(key)
# Qキーの場合、ループ終了
if key == 'q':
break
そうすると処理がmain.pyからmap,pyのMapクラスのmoveメソッドに移りますので、そちらにコードを記述していきます。ファイルが変わっている事に注意してください。
def move(self, key):
"""マップを移動
"""
# 下に移動(Sキー)の場合
if key == 's':
# 元の場所を空地に変える
self.map_lists[self.now_h][self.now_w] = MapItem.EMPTY.value
# 現在位置の行を1つ足す
self.now_h = self.now_h + 1
# 移動先にPに書き換える
self.map_lists[self.now_h][self.now_w] = MapItem.PLAYER.value
map.pyのMapクラスの中にmoveメソッドを追加しました。
これは引数にinput関数で受け付けたkeyを受け取っています。そしてif文でそのkeyの中身を判定しています。もしも「下に移動」のSキーだった場合に「下に移動」の処理を行います。それ以外のキーではなにも処理しません。
8行目の”self.map_lists[][]”はインデックス[1, 1]のように、マップの座標を表しています。プレイヤーの最初の座標を”self.now_h”と”self.now_w”に与えていますので、このコードでは元の位置にあった”P”を”MapItem.EMPTY.value”つまり列挙型の値全角スペースの” ”に置き換えています。
11行目では元の座標に1をプラスして、再度その値を座標として保持しています。最初の”now_h”は1を付与していましたので”1+1”が行われ、その結果の2を保持しています。これはインスタンスに保持されています。
14行目にて再び”self.map_lists[self.now_h][self.now_w]”が登場していますが、self.now_hの値が1プラスされた座標であることに注意が必要です。行が1つ増えているということは、元の座標より下の位置として出力される位置を表しています。その座標に列挙型のMapItem.PLAYER.valueの値”P”を与えています。
以上で、”P”が最初にいた座標には” ”が挿入され、1行下の座標に”P”が挿入されました。
実際に動作確認してみましょう。
想定通りに”P”を下に動かすことができました!
しかし、この”P”は”薬”にぶつかってもアイテムが取得できるわけではないですし、壁として表現している”#”もぶつかるわけでなく通りすぎてしまいます。さらに最下段を通り過ぎるとエラーになります。
という訳で、まだまだ改良予知はありますが、今回はここまでです。
まとめ
今回はインスタンスのメンバに位置座標を持たせ、While文でループ処理とし、受け付けたキーによってプレイヤーを移動させるというところまで来ました。
「プレイヤーを移動させる」ためだけに行わなければいけない処理が多くて驚かれるかもしれませんが、遊ぶ側は当たり前に感じることでも、作る側にまわると大変さがわかるというのはこういうところから来ている訳です。
しかし、これらの記述はPythonの基礎が満載ですので、様々な処理に触れることができ学びも多いと感じています。
今回も最後まで読んで頂きありがとうございました。また次回お会いできることを楽しみにしております。
以下のDiscordサーバでも質問を受け付けています。お気軽に声をかけて頂けると幸いです!