Operation KiWi

一生使える言語はPythonだと信じてる

PythonでもRPG作れるの?という疑問に答えるよ+猫でも分かるRPG作成講座入門編

f:id:sakage24:20170630111613g:plain

これがPythonで実際に作成していたローグライクゲームです。今見ると意外と良く出来てる!

Pythonでゲーム作りってどうなの?

あなたにPython愛があるのなら挑戦してみましょう。あと、一つアドバイスをしておくと、エロいもの作ったほうがやる気上がりますよw

ただ、Pythonでゲーム制作に挑戦するとなるといくつか問題点があります。

ゲーム用のライブラリが少ない・あっても更新・メンテナンスがされていない

pygame

Pythonでゲーム制作するなら、恐らく選択肢に入るのはpygameです。しかしながらこれは2009年から開発が止まっています。しかも、めちゃめちゃ重い上に、ファミコンレベルの作品しか作れません。あと有益な情報はほぼ英語です。

pygameドキュメント

pyglet

pygameの後継としてpygletがあります。こちらはあまり詳しくありませんので、割愛しますが、見た感じ日本語情報はほぼ皆無です。心してかかりましょう

他の言語と比べても、圧倒的に日本語の資料は少ない。英語力が試される

Python自体、日本では相当マイナーな言語ですので(こんなに楽しい言語なのに!)仕方がありませんが、やはり日本語ドキュメントが無いと立ち尽くしてしまいますよね…幸い、最近はgoogle翻訳さんが日進月歩してますので、上手く活用してみて下さい。


Pygame (Python Game Development) Tutorial - 1 - Introduction

私は、この動画を見ながら勉強しました。全100本とかいう狂った量ですが、大体把握できますよ。英語ですけど、ソースコード眺めてたらなんとなく分かります。特に、Pythonで作成したスクリプトをそのままwindows環境で動かすのには、

  1. まず対応するpythonをインストールして…
  2. それから必要な外部ライブラリをpipでインストールして…

という手順を踏んで貰う必要があります。これじゃ誰もやってくれないので、cx_freezeっていうモジュールを使うんですね。それの詳細な解説もしてくれてます。

ハマリポイントなんですが、過去に私の記事でも解説していますので良かったらどうぞ。

www.kiwi-bird.xyz

C言語やJavaなどと比べると遅い。どのくらい遅いかというと

C言語で書いたできの悪いコードと洗練されたアルゴリズムで書いたPythonのコードが戦った場合、もしかしたら勝負になる可能性があるかもしれない(勝てるとは言っていない)

ゲームライブラリがないなら、標準ライブラリだけで作ればいいじゃない

上に挙げたライブラリを使わなくても、ゲームは作れるんですよ。貴方が望むエロいのもねwスプライトとかは使えませんが、それっぽいことは出来るんです。楽しいゲームづくりの世界へようこそ!今回は少しだけ、PythonでRPG制作を体験してみませんか?

猫でも分かるRPG制作講座(ソースコード付き)入門編

これから、簡単なRPGを作っていきます。突発的に思いついたのでやれるところまでw

注意
  • 今回の講座は、windows10でのみ動作を確認しています
    • msvcrtというwindows専用のモジュールを利用するので、Linuxやmacでは動きません。
  • Python3についての基礎的な知識があることを前提にしています。
超注意!!!

PyCharmをご利用の方は、runではなく、Terminalもしくはコマンドプロンプトから

py スクリプト.py

で実行して下さい。Pycharmではmsvcrtが動かないため、正常にプログラムは実行されません。

副読本として、オライリーの「入門Python3」を推薦します。正直、これ無しでPythonプログラミングは不可能なのでwちょっとでも難しいと感じた場合は迷わず購入しましょう。私は購入して1年経ちますが、便利すぎて未だに手放せませんw *1

入門 Python 3

入門 Python 3

今回やること
  • フィールドマップを作ろう
  • プレイヤーを表示しよう
  • 移動が出来るようにしよう

やる気を高めよう

f:id:sakage24:20170630172155g:plain

今回作るのはこんな感じです。フィールドを自由に移動できるようにしましょう。

コピペして実際に動かしてみて下さい。

色々改造したものがgithubに置いてあります。

github.com

注意点としては、msvcrtというwindows専用モジュールを利用してるので、Linux, OS/Xでは動きません。 Windowsユーザー以外の方は、msvcrt.getwch()の部分をinput()とかに置換しましょう。

完成版ソースコード

import pprint
import msvcrt
from colorama import init
import copy

init()


class FieldMap(object):
    __filed_map = [[0 for i in range(10)] for j in range(10)]

    @classmethod
    def get_field_map(cls):
        return cls.__filed_map

    @classmethod
    def set_field_map(cls, x, y, z):
        cls.__filed_map[y][x] = z


class ViewFilter(object):
    @staticmethod
    def get_view(rep, pos):
        for i in range(10):
            for j in range(10):
                if i == pos[0] and j == pos[1]:
                    rep[i][j] = "@"
                elif rep[i][j] == 0:
                    rep[i][j] = "・"
                elif rep[i][j] == 1:
                    rep[i][j] = "村"
        return rep


class Moving(object):
    __player_position_y = 0
    __player_position_x = 0

    def __init__(self, x, y):
        Moving.__player_position_y = y
        Moving.__player_position_x = x

    @classmethod
    def get_player_pos_y(cls):
        return cls.__player_position_y

    @classmethod
    def get_player_pos_x(cls):
        return cls.__player_position_x

    @classmethod
    def move(cls):
        raw_inputs = msvcrt.getwch()
        if raw_inputs == "w":
            cls.__player_position_x -= 1
        elif raw_inputs == "a":
            cls.__player_position_y -= 1
        elif raw_inputs == "s":
            cls.__player_position_x += 1
        elif raw_inputs == "d":
            cls.__player_position_y += 1
        else:
            print("WASD以外のキーを押さないで下さい。")

    @staticmethod
    def position_prints():
        print(Moving.__player_position_y, Moving.__player_position_x)

pp = pprint.PrettyPrinter()
field = FieldMap()
your_position = Moving(x=5, y=5)

# ほげ村の位置
field.set_field_map(x=2, y=3, z=1)

# ぴよ村の位置
field.set_field_map(x=7, y=9, z=1)

while True:
    player_pos = (your_position.get_player_pos_x(), your_position.get_player_pos_y())
    pp.pprint(ViewFilter.get_view(copy.deepcopy(field.get_field_map()), player_pos))
    your_position.move()
    your_position.position_prints()
    print("\033[2J")

事前準備

Python3をインストールして下さい。インストール後、コマンドプロンプトを起動して下記のコマンドを入力して下さい。

pip install colorama

フィールドマップを作ろう

RPGといえばマップが必要です。村があって、城があって、ダンジョンが合ってこそ!

ソースコード

import pprint


class FieldMap(object):
    __filed_map = [[0 for i in range(10)] for j in range(10)]

    @classmethod
    def get_field_map(cls):
        return cls.__filed_map

    @classmethod
    def set_field_map(cls, x, y, z):
        cls.__filed_map[x][y] = z


field = FieldMap()
pp = pprint.PrettyPrinter()
pp.pprint(field.get_field_map())

フィールドマップはクラス化しました。今後も色んな所から呼び出して使うので、クラスメソッドにしてます。クラスメソッドにすると、クラスに加えた変更は全てのオブジェクトに反映されます。

あと、直接値書き換えるのも何か抵抗があるので、setterとgetterをつけてます。

さて、とりあえず何はともあれ、フィールドマップの元となる、二次元配列を用意します。まずは縦10, 横10のマップにしましょう。中身は0埋めで初期化します。pprintはめっちゃ便利なんで覚えておきましょう。配列を出力する際など、人間に見やすいように整形してくれます。

結果

[[0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
 [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
 [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
 [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
 [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
 [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
 [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
 [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
 [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
 [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]]

マップが出来上がりました!!村、街、城、ダンジョン、魔王の城…色々作りたいですが、とりあえず必要な草原を用意しましょう。

2次元配列内の値がそれぞれ

  • 0は草原。
  • 1は村。
  • プレイヤーは@

とします。上記の例に沿ってプログラムを少し書き換えてみましょう。

ソースコード

import pprint


class FieldMap(object):
    __filed_map = [[0 for i in range(10)] for j in range(10)]

    @classmethod
    def get_field_map(cls):
        return cls.__filed_map

    @classmethod
    def set_field_map(cls, x, y, z):
        cls.__filed_map[x][y] = z


field = FieldMap()
pp = pprint.PrettyPrinter()

# ほげ村の位置
field.set_field_map(x=2, y=3, z=1)

# ぴよ村の位置
field.set_field_map(x=7, y=9, z=1)

pp.pprint(field.get_field_map())

結果

[[0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
 [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
 [0, 0, 0, 1, 0, 0, 0, 0, 0, 0],
 [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
 [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
 [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
 [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
 [0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
 [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
 [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]]

出来てますね!値はキチンと入っていますが、見栄えが悪いです。無機質な数字がズラッと並んでいて面白くないので、数値を文字列に変換して表示するようにしましょう。

ソースコード

import pprint
import copy


class FieldMap(object):
    __filed_map = [[0 for i in range(10)] for j in range(10)]

    @classmethod
    def get_field_map(cls):
        return cls.__filed_map

    @classmethod
    def set_field_map(cls, x, y, z):
        cls.__filed_map[x][y] = z


class ViewFilter(object):
    @staticmethod
    def get_view(rep):
        for i in range(10):
            for j in range(10):
                if rep[i][j] == 0:
                    rep[i][j] = "・"
                elif rep[i][j] == 1:
                    rep[i][j] = "村"

        return rep

field = FieldMap()
pp = pprint.PrettyPrinter()

# ほげ村の位置
field.set_field_map(x=2, y=3, z=1)
# ぴよ村の位置
field.set_field_map(x=7, y=9, z=1)
pp.pprint(ViewFilter.get_view(copy.deepcopy(field.get_field_map())))

結果

[['・', '・', '・', '・', '・', '・', '・', '・', '・', '・'],
 ['・', '・', '・', '・', '・', '・', '・', '・', '・', '・'],
 ['・', '・', '・', '村', '・', '・', '・', '・', '・', '・'],
 ['・', '・', '・', '・', '・', '・', '・', '・', '・', '・'],
 ['・', '・', '・', '・', '・', '・', '・', '・', '・', '・'],
 ['・', '・', '・', '・', '・', '・', '・', '・', '・', '・'],
 ['・', '・', '・', '・', '・', '・', '・', '・', '・', '・'],
 ['・', '・', '・', '・', '・', '・', '・', '・', '・', '村'],
 ['・', '・', '・', '・', '・', '・', '・', '・', '・', '・'],
 ['・', '・', '・', '・', '・', '・', '・', '・', '・', '・']]

なんかキモいですけど、とりあえず体裁は整えました。'',が邪魔なんですが、pprintを使うと消せないので、消したい方はfor文とprint()を使って出力する方式にしてみて下さい。

オブジェクトをコピーする時は、copy.deepcopy()をしないと後々痛い目を見るので注意。訳の分からないエラーで時間を無駄にすることになります…w

プレイヤーを表示しよう

何かが足りません…そう!プレイヤーがいないです。プレイヤーがいないとゲームが始まらないですので、早急に作りましょう。ただ、一つ問題があって、今までの感じで、プレイヤーをそのまま2次元配列の(field_map)に入れていいのか?ということです。

当然、プレイヤーは移動します。そうすると、その都度配列内の値をプレイヤーの値にすべて書き換えてしまうことになり、マップが@で埋め尽くされてしまいます。

この事態を避けるため、プレイヤーの座標は別のクラスで保持して、マップ上にはViewFilterクラスを使って見かけだけ反映するようにします。何を言っているのか分かりませんね…やってみましょう。

ViewFilterのget_view()に条件式を追加しよう。プレイヤーの現在位置、座標を管理するmovingクラスを定義しよう。

ソースコード

import pprint
import copy


class FieldMap(object):
    __filed_map = [[0 for i in range(10)] for j in range(10)]

    @classmethod
    def get_field_map(cls):
        return cls.__filed_map

    @classmethod
    def set_field_map(cls, x, y, z):
        cls.__filed_map[x][y] = z


class ViewFilter(object):
    @staticmethod
    def get_view(rep, pos): # 引数追加
        for i in range(10):
            for j in range(10):
                if i == pos[0] and j == pos[1]: # 条件式追加
                    rep[i][j] = "@"
                if rep[i][j] == 0:
                    rep[i][j] = "・"
                elif rep[i][j] == 1:
                    rep[i][j] = "村"

        return rep


class Moving(object):
    __player_position_y = 0
    __player_position_x = 0

    def __init__(self, x, y):
        Moving.__player_position_y = y
        Moving.__player_position_x = x

    @classmethod
    def get_player_pos_y(cls):
        return cls.__player_position_y

    @classmethod
    def get_player_pos_x(cls):
        return cls.__player_position_x

    @staticmethod
    def position_prints():
        print(Moving.__player_position_y, Moving.__player_position_x)

field = FieldMap()
pp = pprint.PrettyPrinter()

# ほげ村の位置
field.set_field_map(x=2, y=3, z=1)
# ぴよ村の位置
field.set_field_map(x=7, y=9, z=1)
# プレイヤーの位置
your_position = Moving(x=5, y=5) # 追加
player_pos = (your_position.get_player_pos_x(), your_position.get_player_pos_y()) # 追加
pp.pprint(ViewFilter.get_view(copy.deepcopy(field.get_field_map()), player_pos)) # 追加

この状態で一回走らせてみましょう。

結果

[['・', '・', '・', '・', '・', '・', '・', '・', '・', '・'],
 ['・', '・', '・', '・', '・', '・', '・', '・', '・', '・'],
 ['・', '・', '・', '村', '・', '・', '・', '・', '・', '・'],
 ['・', '・', '・', '・', '・', '・', '・', '・', '・', '・'],
 ['・', '・', '・', '・', '・', '・', '・', '・', '・', '・'],
 ['・', '・', '・', '・', '・', '@', '・', '・', '・', '・'],
 ['・', '・', '・', '・', '・', '・', '・', '・', '・', '・'],
 ['・', '・', '・', '・', '・', '・', '・', '・', '・', '村'],
 ['・', '・', '・', '・', '・', '・', '・', '・', '・', '・'],
 ['・', '・', '・', '・', '・', '・', '・', '・', '・', '・']]

プレイヤーである、@が表示されました!当然、移動する機能はまだ持っていないので、動きません。

注意点として、この@は2次元配列を直接書き換えたものではなく、ViewFilter().get_view()関数によって見かけ上だけ書き換えられているものだということを理解してください。

移動出来るようにしよう

移動はWASD(上左下右)キーで行うこととします。実際に移動のための座標、座標操作を受け持つMovingクラスを追加します。Moving().move()関数は、wasd何れかのキーが押下された場合、__player_position_y(x) に対して加算減算処理を行います。

入力を伴う処理なので、input()を使っても良いのですが、input()はENTERを押さないと出力が確定されないので、テンポが悪いです。

import msvcrt


class Moving(object):
    __player_position_y = 0
    __player_position_x = 0

    def __init__(self, x, y):
        Moving.__player_position_y = y
        Moving.__player_position_x = x

    @classmethod
    def get_player_pos_y(cls):
        return cls.__player_position_y

    @classmethod
    def get_player_pos_x(cls):
        return cls.__player_position_x

    @staticmethod
    def position_prints():
        print(Moving.__player_position_y, Moving.__player_position_x)

    ### 追加 ###
    @classmethod
    def move(cls):
        raw_inputs = msvcrt.getwch()
        if raw_inputs == "w":
            cls.__player_position_x -= 1
        elif raw_inputs == "a":
            cls.__player_position_y -= 1
        elif raw_inputs == "s":
            cls.__player_position_x += 1
        elif raw_inputs == "d":
            cls.__player_position_y += 1
        else:
            print("WASD以外のキーを押さないで下さい。")

ゲームをループさせよう

このままだと、一回移動しただけでプログラムが終了してしまいます。そのため、処理を繰り返す必要があります。

ゲームの場合は、プレイヤーが終了しない限り終わりが無いので、無限ループ内で実行することになります。

import pprint
import msvcrt
from colorama import init
import copy


class FieldMap(object):
    __filed_map = [[0 for i in range(10)] for j in range(10)]

    @classmethod
    def get_field_map(cls):
        return cls.__filed_map

    @classmethod
    def set_field_map(cls, x, y, z):
        cls.__filed_map[y][x] = z


class ViewFilter(object):
    @staticmethod
    def get_view(rep, pos):
        for i in range(10):
            for j in range(10):
                if i == pos[0] and j == pos[1]:
                    rep[i][j] = "@"
                elif rep[i][j] == 0:
                    rep[i][j] = "・"
                elif rep[i][j] == 1:
                    rep[i][j] = "村"
        return rep


class Moving(object):
    __player_position_y = 0
    __player_position_x = 0

    def __init__(self, x, y):
        Moving.__player_position_y = y
        Moving.__player_position_x = x

    @classmethod
    def get_player_pos_y(cls):
        return cls.__player_position_y

    @classmethod
    def get_player_pos_x(cls):
        return cls.__player_position_x

    @classmethod
    def move(cls):
        raw_inputs = msvcrt.getwch()
        if raw_inputs == "w":
            cls.__player_position_x -= 1
        elif raw_inputs == "a":
            cls.__player_position_y -= 1
        elif raw_inputs == "s":
            cls.__player_position_x += 1
        elif raw_inputs == "d":
            cls.__player_position_y += 1
        else:
            print("WASD以外のキーを押さないで下さい。")

    @staticmethod
    def position_prints():
        print(Moving.__player_position_y, Moving.__player_position_x)

pp = pprint.PrettyPrinter()
field = FieldMap()
your_position = Moving(x=5, y=5)

# ほげ村の位置
field.set_field_map(x=2, y=3, z=1)

# ぴよ村の位置
field.set_field_map(x=7, y=9, z=1)

### 追加 ###
while True:
    player_pos = (your_position.get_player_pos_x(), your_position.get_player_pos_y())
    pp.pprint(ViewFilter.get_view(copy.deepcopy(field.get_field_map()), player_pos))
    your_position.move()

実行結果

f:id:sakage24:20170630183630g:plain

…動いたけど、なんかおかしいですよねw直していきましょう。

coloramaとエスケープシーケンス

上記の現象を回避するには、出力一回ごとに画面をクリアする必要があります。(疲れてきたので)手短に説明しますが、

from colorama import init


init()
print("\033[2J")

これで画面クリアが出来ます。よって完成版ソースは以下の通りとなります。

完成

import pprint
import msvcrt
from colorama import init
import copy

init()


class FieldMap(object):
    __filed_map = [[0 for i in range(10)] for j in range(10)]

    @classmethod
    def get_field_map(cls):
        return cls.__filed_map

    @classmethod
    def set_field_map(cls, x, y, z):
        cls.__filed_map[y][x] = z


class ViewFilter(object):
    @staticmethod
    def get_view(rep, pos):
        for i in range(10):
            for j in range(10):
                if i == pos[0] and j == pos[1]:
                    rep[i][j] = "@"
                elif rep[i][j] == 0:
                    rep[i][j] = "・"
                elif rep[i][j] == 1:
                    rep[i][j] = "村"
        return rep


class Moving(object):
    __player_position_y = 0
    __player_position_x = 0

    def __init__(self, x, y):
        Moving.__player_position_y = y
        Moving.__player_position_x = x

    @classmethod
    def get_player_pos_y(cls):
        return cls.__player_position_y

    @classmethod
    def get_player_pos_x(cls):
        return cls.__player_position_x

    @classmethod
    def move(cls):
        raw_inputs = msvcrt.getwch()
        if raw_inputs == "w":
            cls.__player_position_x -= 1
        elif raw_inputs == "a":
            cls.__player_position_y -= 1
        elif raw_inputs == "s":
            cls.__player_position_x += 1
        elif raw_inputs == "d":
            cls.__player_position_y += 1
        else:
            print("WASD以外のキーを押さないで下さい。")

    @staticmethod
    def position_prints():
        print(Moving.__player_position_y, Moving.__player_position_x)

pp = pprint.PrettyPrinter()
field = FieldMap()
your_position = Moving(x=5, y=5)

# ほげ村の位置
field.set_field_map(x=2, y=3, z=1)

# ぴよ村の位置
field.set_field_map(x=7, y=9, z=1)

while True:
    player_pos = (your_position.get_player_pos_x(), your_position.get_player_pos_y())
    pp.pprint(ViewFilter.get_view(copy.deepcopy(field.get_field_map()), player_pos))
    your_position.move()
    your_position.position_prints()
    print("\033[2J")

終わり

超疲れた…16500文字ですよ…wほとんどソースコードですがw 多分ミスがめっちゃあるんですが、ソースは一応起動確認してます。何かあればコメントでお願いします。

正直めちゃめちゃ大変だった…結局ソースコードほぼ書き直しているしw

皆様のお役に立てましたでしょうか?入門Pythonおすすめです!!!あとEffective Pythonは神

入門 Python 3

入門 Python 3

Effective Python ―Pythonプログラムを改良する59項目

Effective Python ―Pythonプログラムを改良する59項目

初心者向けの方へ、RPGゲームを作っていく記事を書いています

www.kiwi-bird.xyz

*1:勇者よ、聞こえましたか?さあ早く買うのです…