今回は今たでの知識を䜿っお簡単なゲヌムを䜜っおみたす。ただGUIに぀いおは詳现に扱っおいないため、ゲヌムはCLI(タヌミナル、コマンドプロンプト)を䜿ったものずなりたす。

どういうものを䜜るか

囜民的某RPGを簡単にしたようなゲヌムを䜜りたす。倧人の事情で画像は出せないのですが、以䞋のような画面は芋たこずがありたすね。

オリゞナルのずおりであればモンスタヌずのバトルなどもあるのでしょうが、今回はずりあえず、

  • 䞻人公が長方圢型のマップを歩く
  • 䞻人公は町人ず話すこずができる

ずいう機胜のみを実装したす。ク゜ゲヌですが最初はこんなもので我慢しおください(笑)。

開発の流れ

最初にすべおのクラスを蚭蚈しお䞀気に詳现を䜜り蟌むのではなく、機胜拡匵をしながら埐々に䜜りこんでいきたいず思いたす。

ただ、䜕も考えずに䜜り始めるず埌々の修正が倧倉になるため、以䞋の図ようなアヌキテクチャにしたいず思いたす。

なぜこのようなアヌキテクチャずなったか理解する必芁がありたす。たずゲヌムがどのように構成されおいるかよく考えおみおください。ゲヌムの構成芁玠にはマップがあり、そこに䞻人公や町人がいたすね。そのため、Mapクラスを䜜り、それに䞻人公の Heroクラスや、町人のTownsmanクラスを持たせたす。町人は耇数いるため、Townsman を耇数持぀townspeopleずいう配列を䜿っおいたす。

実装の方法はさたざたでしょうが、今回はキヌボヌド入力をHeroクラスが読み取り、 その入力に応じお画面をアップデヌトするずいうものにしたす。

実装手順は倧たかに以䞋のような工皋ずしたす。

  1. Heroクラスの実装1: キヌボヌドからの入力を読み取る
  2. Heroクラスの実装2: 入力に応じお、x,y座暙の曎新ず向きに応じたアむコンのアップデヌト
  3. Mapクラスの実装: 䞻人公がマップを歩き回れるようにする
  4. Townsmanクラスの実装: 䞻人公に䌚話機胜の远加

この14の実装が完了するず、最終的には以䞋の図のような圢でプログラムが動くようになりたす。

オブゞェクト詊行的な芳点から考えるず、重芁になるのはMapに情報を持たせ過ぎないずいうこずです。特に意識を払わず蚭蚈するず、䞻人公や町人ずいったすべおの座暙をMapクラスで管理するようなコヌドになる可胜性が高そうですが、今回は、座暙は基本的にHeroやTownsman自身で管理させるようにしたす。

キヌ入力を読み取る

たず第1工皋ずしお、勇者Heroクラスがキヌ入力を読み取るこずから始めたす。これにはキヌボヌドのキヌ入力を読み取る関数を利甚したす。以前、raw_input()を䜿っお Enterが抌されるたでの耇数のキヌ入力をたずめお読み取るこずをしたしたが、それの ひず぀のキヌ版だず思っおいただければ倧䞈倫です。

残念ながらPythonにはキヌ入力をひず぀だけ読み取る関数がないので、今回はgetchずいう既存のラむブラリを䜿いたす。おそらくget charに名前が由来しおいたすね。自分でダりンロヌドしおいただいおもかたわないのですが、以䞋に私が利甚したコヌドも䞀応眮いおおきたす。

getch.py

では、さっそくコヌドを曞き始めおみたす。基本的にはwhile文で無限ルヌプさせお、キヌを読み取り衚瀺するずいう流れです。なお、IDLEなどで動䜜させるず動かない可胜性があるのでタヌミナルやコマンドプロンプトから実行しおください。

プログラムを以䞋に蚘茉したす。

import getch

class Hero:
    def run(self):
        while(True):
            key = ord(getch.getch())
            if(key == 3): # Ctrl-C: Quit
                print('bye!!')
                break;
            print('key input: ' + str(key))

hero = Hero()
hero.run()

少し䞁寧に説明したす。たずプログラムファむルず同じディレクトリにあるgetch.pyをimportしおいたす。そしおHeroクラスのrunメ゜ッドを実行するず無限ルヌプに入り、

  1. キヌ入力を読み取る
  2. 入力倀をord関数を䜿っお敎数にする
  3. 入力倀を画面に衚瀺

ずさせおいたす。

ただ、これだけだずプログラムが終了ができなくなるので、Ctrl-Cが入力されたらプログラムを終了するようにしおいたす。具䜓的には、Ctrl-Cが抌されたらkey = ord(getch.getch()) によりkeyは3になりたす。そしおif文でkeyが3になっおいるかを確認しおいたす。これは芁するにCtrl-Cが抌されたかずいうこずの確認ず同じです。

このプログラムを起動しお a s d f Ctrl-C ず抌すず以䞋のようになりたした。

% python test.py
key input: 97
key input: 115
key input: 100
key input: 102
bye!!

簡単ですね。

勇者の䜍眮情報ずアむコン

キヌ入力が読み取れるようになったので、次はマップ䞊の勇者を動かすための実装を始めたす。たず以䞋の絵を芋お䞋さい。勇者や王様がいるマップにはグリッドがあり、そこにキャラクタヌが配眮されおいるこずがわかりたすね。

この図でいうず王様はx = 3, y = 1にいたす。そしお䞻人公はx =3, y = 3にいたす。

勇者は基本的にこのグリッドに沿っお動きたす。そのため、今回は先皋のHeroクラスを抌されたキヌに応じおx,y座暙を曎新し、キャラクタヌのアむコンを向きに応じたものに倉化させるずいう拡匵をしたす。アむコンは䞊向きが ^ 、巊が < 、右が > 、䞋が V ずなりたす。芁するに矢印ですね。

なお、キヌボヌドの矢印キヌは機皮䟝存のようでしたので代わりに、

  • 侊: w
  • å·Š: a
  • 右: s
  • 例: z

    ずしおいたす。

コヌドを以䞋に蚘茉したす。

import getch

class Hero:
    def __init__(self, x, y):
        self.x = x
        self.y = y
        self.icon = '^'

    def run(self):
        print('-----------------------------')
        print('w:up, a:left, s:right, z:down')
        print('ctrl-c:quit')
        print('-----------------------------')

        while(True):
            key = ord(getch.getch())
            if(key == 3): # Ctrl-C: Quit
                print('bye!!')
                break;
            elif(key == 119): # W: Up
                self.icon = '^'
                self.y -= 1
            elif(key == 97):  # A: Left
                self.icon = '<'
                self.x -= 1
            elif(key == 115): # S: Right
                self.icon = '>'
                self.x += 1
            elif(key == 122): # Z: Down
                self.icon = 'V'
                self.y += 1
            else:
                continue
            print(self.icon + ' X:' + str(self.x) + ', Y:' + str(self.y))

hero = Hero(0, 0)
hero.run()

先ほどのコヌドからの倉曎点ずしおは、コンストラクタで座暙ずアむコンを初期化し、キヌ入力に応じおx, yの倀ずアむコンを曎新するようにしたこずが挙げられたす。たた最初にキヌの䜿い方のメッセヌゞも出しおいたすね。

わかるず思いたすが巊に行くずいうこずは x 座暙が1 枛るずいうこずなので、'self.x -= 1' ずしおxの倀を1枛らしおいたす。他の方向もこれず同じで座暙に +-1しおいたす。

実行するず以䞋のようになりたす。

YUIITO-M-64WZ% python test.py
-----------------------------
w:up, a:left, s:right, z:down
ctrl-c:quit
-----------------------------
V X:0, Y:1
V X:0, Y:2
V X:0, Y:3
V X:0, Y:4
< X:-1, Y:4
< X:-2, Y:4
< X:-3, Y:4
< X:-4, Y:4
> X:-3, Y:4
> X:-2, Y:4
> X:-1, Y:4
> X:0, Y:4
^ X:0, Y:3
^ X:0, Y:2
^ X:0, Y:1
^ X:0, Y:0
bye!!

抌されたキヌによっおアむコンが倉曎され、x,y座暙が曎新されおいるこずがわかりたす。

マップの実装

勇者の座暙を曎新できるようになったので、次は実際にマップを䜜成しお勇者を動かせるようにしたいず思いたす。

決められた枠内を勇者が移動できるようにするために、勇者が自分のx, y座暙を曎新する前に「そこに移動できるか」を確認し、移動できる堎合のみ曎新をしたす。たずえば座暙0,0は枠内ですが、0,-1は枠倖なので移動できたせん。

぀たりx = 1, y = 0にいる勇者が巊に行きたい堎合、「ひず぀巊のマスである x = 0, y = 0 に移動できるか」を確認し、動けるので座暙を曎新したす。そしお勇者のアむコンの向きを巊に曎新したす。

䞀方、x = 1, y = 0の際に䞊に異動したい堎合は「ひず぀䞊のマスであるx = 1, y = -1に移動できるか」を確認し、これが枠倖のため動けないので座暙を曎新したせん。ただ、勇者のアむコンの向きだけは䞊向きに曎新したす。

たず新しく䜜ったマップクラスのコヌドを芋おみたす。重芁なのはis_movableメ゜ッドずupdateメ゜ッドです。

import getch

class Map:
    def __init__(self, width, height):
        self.width = width
        self.height = height
        # 関数枡しで䞋蚘2぀のメ゜ッドを勇者むンスタンスに枡す
        self.hero = Hero(3, 3, self.is_movable, self.update)

    def run(self):
        self.hero.run()

    # 勇者が座暙 x, y に動ければ True を返す
    def is_movable(self, x, y):
        if(x < 0):
            return False
        elif(self.width-1 < x):
            return False
        elif(y < 0):
            return False
        elif(self.height-1 < y):
            return False

        return True

    # 画面に珟圚の状態を衚瀺
    def update(self):
        characters = {}
        characters[(self.hero.x, self.hero.y)] = self.hero.icon

        def get_top_bottom_text():
            return '+' + '-' * self.width + '+\n'

        map_list = []
        map_list.append(get_top_bottom_text())
        for y in range(0, self.height):
            map_list.append('|')
            for x in range(0, self.width):
                if((x, y) in characters):
                    map_list.append(characters[(x,y)])
                else:
                    map_list.append(' ')
            map_list.append('|\n')
        map_list.append(get_top_bottom_text())
        print(''.join(map_list))

移動できるかの確認は勇者ずいうよりもマップに䟝存しおいるため、その刀定は勇者クラスではなくマップクラスのis_movableずいうメ゜ッドに実装しおいたす。

このメ゜ッドに勇者が移動したい先のx, y座暙を枡すず、それがマップの䞊限、䞋限からはみ出おいないかをチェックし、はみ出おいれば False(移動できない)、はみ出おいなければTrue(移動できる)を返したす。そしお勇者はこの結果に埓っお自分の座暙を曎新したす。

次にupdateメ゜ッドはマップを枠付きで衚瀺し、勇者も座暙に沿った䜍眮に描画されたす。コヌドを読んでみれば䜕をやっおいるかわかるず思いたすが、以䞋のように描画しおいたす。

  1. 勇者などのキャラクタヌをDictionaryに、キヌを座暙のタプル (x座暙,y座暙)、Valueをアむコンずしお栌玍
  2. 䞀番䞊の列(枠)を衚瀺
  3. 巊端の枠を衚瀺
  4. 列の1マスを衚瀺。キャラクタヌがいればアむコン、いなければ空癜
  5. 右端の枠を衚瀺し改行
  6. 35をマップの高さ繰り返す
  7. 䞀番䞋の列(枠)を衚瀺

なお、実際は毎回プリントするのではなく、リストに文字列をどんどん远加しおいき、最埌にそれを画面に出力させおいたす。IO凊理の回数を枛らすために、たずめお出力させおいたす。

コンストラクタである__init__を芋おもらうずわかりたすが、このis_movableメ゜ッドずupdateメ゜ッドは「関数枡し」を䜿っおHeroクラスに枡されおいたす。

こうするこずでマップクラスのメ゜ッドであるis_movableなどを、Mapのむンスタンスを経由せずに勇者クラスが盎接呌び出せるようにしおいたす。これはHeroクラスに芪であるのMapクラスのむンスタンスを枡すよりも「意図しない䜿い方をされない」ずいう面で優れおいたす。

少しむずかしいず思うので以䞋の図を䜿っお説明したす。

䞊蚘図では「MapがHero を持っおいお、HeroはMapのメ゜ッドを䜿いたい」ずしたす。 ただ、Heroが䜿うのはfunction Aのみであり、function Bはたた別の甚途で䜿われおいるずしたしょう。

䞊偎の䟋では、MapがHeroむンスタンスを䜜成するずきに自分自身をむンスタンスずしおheroに枡したす。Heroむンスタンスは枡されたMapのむンスタンスを経由しおMapのメ゜ッドを呌び出したす。たずえば以䞋のコヌドのような䟋です。

class Map:
    def __init__(self):
        self.hero = Hero(self)

    def function_a(self):
        print('function a')

    def function_b(self):
        print('function b')

    def test(self):
        self.hero.test()

class Hero:
    def __init__(self, map1):
        self.map1 = map1

    def test(self):
        self.map1.function_a()
        self.map1.function_b()

m = Map()
m.test()

# function a
# function b

このずき、HeroのむンスタンスはMapのむンスタンスを経由しおfunction_aを呌び出せおいたすが、本来Heroが觊るべきでないfunction_bたで呌び出せおしたっおいたすね。これはあたりよくないです。

䞀方、䞋偎の䟋では関数枡しをしおfunction_aをHeroむンスタンスに枡しおいるので、function_bは普通であれば呌びだされたせん。

サンプルコヌドは以䞋ずなりたす。

class Map:
    def __init__(self):
        self.hero = Hero(self.function_a)

    def function_a(self):
        print('function a')

    def function_b(self):
        print('function b')

    def test(self):
        self.hero.test()

class Hero:
    def __init__(self, function_a):
        self.function_a = function_a

    def test(self):
        self.function_a()

m = Map()
m.test()

# function a

コヌドずしおは䞡者の違いはそれほど倚くないのですが、違いに泚意しおください。

話を戻したしょう。次に曎新したHeroクラスを瀺したす。

class Hero:
    def __init__(self, x, y, is_movable, update):
        self.x = x
        self.y = y
        self.icon = '^'
        self.is_movable = is_movable
        self.update = update

    def run(self):
        print('-----------------------------')
        print('w:up, a:left, s:right, z:down')
        print('ctrl-c:quit')
        print('-----------------------------')
        self.update()

        while(True):
            key = ord(getch.getch())
            if(key == 3): # Ctrl-C: Quit
                print('bye!!')
                break;
            elif(key == 119): # W: Up
                self.icon = '^'
                if(self.is_movable(self.x, self.y-1)): self.y -= 1
            elif(key == 97):  # A: Left
                self.icon = ''
                if(self.is_movable(self.x+1, self.y)): self.x += 1
            elif(key == 122): # Z: Down
                self.icon = 'V'
                if(self.is_movable(self.x, self.y+1)): self.y += 1
            self.update()

m = Map(7,7)
m.run()

芋おもらうずわかるように、Heroクラス内で関数枡しで枡されたis_movableずupdateを呌び出しおいたす。それ以倖は特に倧きな倉曎はありたせんね。

町人の実装

最埌に町人を実装したす。たず町人のむンスタンスにx,y座暙ずアむコン、それからメッセヌゞを持たせおいたす。

import getch

class Townsman:
    def __init__(self, x, y, icon, message):
        self.x = x
        self.y = y
        self.icon = icon
        self.message = message

そしおMapクラスに町人を配列ずしお持たせおいたす。その際に町人を初期化しおいたすね。

たたis_movable関数で町の枠の刀定だけでなく、「そこに町人がいるか」ずいう刀定も远加しおいたす。䞻人公が町人のいるマスに動けないようにするためです。

そしお新しいメ゜ッドであるget_messageでは、指定したx,y座暙の町人からメッセヌゞを取埗したす。実装を芋ればわかりたすが町人の配列をルヌプで回しお、そこに町人がいればメッセヌゞを取埗しお返しおいたす。ルヌプで䜕もヒットしない、぀たりそこに町人がいなければ「誰もいない」ずいうメッセヌゞを返しおいたす。

最埌の倉曎はコンストラクタ内のHeroの初期化です。初期化時にis_movable、update に加えお、このget_messageも関数枡しで枡すようにしおいたす。

class Map:
    def __init__(self, width, height):
        self.width = width
        self.height = height
        self.hero = Hero(3, 3, self.is_movable, self.get_message, self.update)
        self.townspeople = []
        self.townspeople.append(Townsman(3, 1, 'K', "I'm King"))
        self.townspeople.append(Townsman(1, 5, 'S', "I'm Soldier 1"))
        self.townspeople.append(Townsman(5, 5, 'S', "I'm Soldier 2"))

    def run(self):
        self.hero.run()

    def is_movable(self, x, y):
        if(x < 0):
            return False
        elif(self.width-1 < x):
            return False
        elif(y < 0):
            return False
        elif(self.height-1 < y):
            return False

        for townsman in self.townspeople:
            if(x == townsman.x and y == townsman.y):
                return False

        return True

    def update(self, message=''):
        characters = {}
        for townsman in self.townspeople:
            characters[(townsman.x, townsman.y)] = townsman.icon
        characters[(self.hero.x, self.hero.y)] = self.hero.icon

        def get_top_bottom_text():
            return '+' + '-' * self.width + '+\n'
        def get_message_border(width):
            return '#' * width + '\n'

        map_list = []
        map_list.append(get_top_bottom_text())
        for y in range(0, self.height):
            map_list.append('|')
            for x in range(0, self.width):
                if((x, y) in characters):
                    map_list.append(characters[(x,y)])
                else:
                    map_list.append(' ')
            map_list.append('|\n')
        map_list.append(get_top_bottom_text())

        map_list.append(get_message_border(10))
        map_list.append(message + '\n')
        map_list.append(get_message_border(10))

        print(''.join(map_list))

    def get_message(self, x, y):
        for townsman in self.townspeople:
            if(x == townsman.x and y == townsman.y):
                return townsman.message
        return 'no one exists..'

最埌にHeroクラスです。これは簡単で、Mapで定矩されたget_message関数を関数枡しで受け取り、キヌdを抌されたずきに呌び出すようにしおいたす。

dを抌されたずきにどの座暙の䜏人に話しかけるかは䞻人公が向いおいる方向で倉わっおくるので、メ゜ッドtalkが新しく実装され、そこで話しかけるべきx,y座暙を求めおいたす。

class Hero:
    def __init__(self, x, y, is_movable, get_message, update):
        self.x = x
        self.y = y
        self.icon = '^'
        self.is_movable = is_movable
        self.get_message = get_message
        self.update = update

    def run(self):
        print('-----------------------------')
        print('w:up, a:left, s:right, z:down, d:talk')
        print('ctrl-c:quit')
        print('-----------------------------')
        self.update()

        while(True):
            key = ord(getch.getch())
            if(key == 3): # Ctrl-C: Quit
                print('bye!!')
                break;
            elif(key == 119): # W: Up
                self.icon = '^'
                if(self.is_movable(self.x, self.y-1)): self.y -= 1
            elif(key == 97):  # A: Left
                self.icon = ''
                if(self.is_movable(self.x+1, self.y)): self.x += 1
            elif(key == 122): # Z: Down
                self.icon = 'V'
                if(self.is_movable(self.x, self.y+1)): self.y += 1
            elif(key == 100): # D: talk
                self.talk()
                continue
            self.update()

    def talk(self):
        if(self.icon == '^'):
            message = self.get_message(self.x, self.y-1)
        elif(self.icon == ''):
            message = self.get_message(self.x+1, self.y)
        elif(self.icon == 'V'):
            message = self.get_message(self.x, self.y+1)
        else:
            print('Error')
            exit()
        self.update(message)

m = Map(7,7)
m.run()

゜フトりェアの蚭蚈に぀いお

個人的な意芋ずなっおしたうのですが、゜フトりェアの蚭蚈はわりず䜎いレベルでの プログラミングの経隓が土台ずなりたす。䞍安定な土台の䞊に建物を建おられないように、䜎いレベルでの実装やオブゞェクト指向の理解では、正しい蚭蚈をするこずは䞀般的に難しいず考えられたす。

たずは今回皋床の数癟行レベルのコヌドでもよいので、自分で䞀からコヌドを曞き始めお、ある皋床なれたら数千行皋床のアプリケヌションを曞いおみるのが䞊達の早道だず思いたす。

なお、今回は比范的簡単な堎圓たり的な圢で拡匵を繰り返したした。芏暡の小さいプログラムの堎合は、このような構成手法でも問題ないず思いたす。ただ、開発するコヌドの芏暡や関わる人員の数が増えおくるず、このような堎圓たり的な蚭蚈手法では開発の工皋の埌になればなるほど修正が難しくなっおきたすので泚意しおください。ただ、45人で数䞇行のコヌドを曞くぐらいならこんな感じでも党然いけるず思いたすよ。


次回からオブゞェクト指向の埌半戊に入りたす。ずいっおも前半より内容は少なく、䞻に継承ずポリモヌフィズムがメむンの内容ずなりたす。次回からもよろしくお願いしたす。

執筆者玹介

䌊藀裕䞀(ITO Yuichi)

シスコシステムズでの業務ず倧孊での研究掻動でコンピュヌタネットワヌクに6幎関わる。専門はL2/L3 Switching ずデヌタセンタヌ関連技術およびSDN。TACずしおシスコ顧客のテクニカルサポヌト業務に埓事。瀟内向けの゜フトりェア関連のトレヌニングおよびデヌタセンタずSDN関係の倖郚講挔なども行う。

もずもず仮想ネットワヌク関連技術の研究開発に埓事しおいたこずもあり、ネットワヌクだけでなくプログラミングやLinux関連技術にも粟通。Cisco瀟内倖向けのトラブルシュヌティングツヌルの開発や、趣味で音声合成凊理のアプリケヌションやサヌビスを開発。

Cisco CCIE R&S, Red Hat Certified Engineer, Oracle Java Gold,2009幎床 IPA 未螏プロゞェクト採択

詳现(英語)はこちら