Ren'Pyで2D RPGを作る(2)―パーティーとステータス

# 概要
こんにちは、しがないエロゲーマー蟹丸です。Ren'Pyを使った2D RPG制作第二回です。
今回はパーティーとステータスを作っていきたいと思います。

前回：[Ren'Pyで2D RPGを作る(1)](https://qiita.com/salt_crab/items/53b241fe1bc8298540f1)

**本記事の範囲**
Ren'Pyでパーティーとステータスの管理ができるようにする、ステータス画面を作る

# 背景
RPGと言ったらまずバトルがしたい！と思うんですが、その前に必要なものがあります。**ステータス**です。体力も攻撃力もわからないままバトルはしたくないですからね。
またDQ1みたいなケースを除いて**パーティー**が必要になってくるでしょう。
ステータスは一人分作ったら同じようにほかの仲間の分も制作できると思うので、このあたりを一緒に進めていきます。

# 1. デフォルトでできることを確認する
## システム実装
ビジュアルノベルであれば好感度を使う場合があるので、ステータス管理がデフォルトであるかも？と思ったのですが、見た感じRen'Pyコード中に`affection`という変数を定義してそこで操作しているようでした。
登場人物数人の好感度を管理するのであればそれで十分ですが、多くのキャラクターと敵に共通する要素であるステータスをこれで実装するのは大変です。わかりやすい実装をする必要がありそうです。
## UI周り
ゲームプレイ中にメニューを開いて仲間の状態やステータスが見られると便利です。
デフォルトのゲームメニューを見てみると、ヘルプが「上部に表示されているキーボード/マウス(/ゲームパッド)をクリックするとそれぞれに対応するヘルプが表示される」というUIになっています。
![SnapCrab_test_2020-7-26_9-33-10_No-00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/679910/12172576-e9dd-0b83-4e8a-a9f56de0d520.png)
![SnapCrab_test_2020-7-26_9-33-20_No-00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/679910/a4e5a679-d154-3b9d-19a4-eaa27b3df2e4.png)
これを利用したら「パーティーメンバーに対してそれぞれのステータスを表示する」ということができそうです。

# 2. 作る
## システム実装
この部分はRen'PyじゃなくてただのPythonだろ！という実装になっているので、Pythonは使えるよ、という方は流し見してください。
### Statsクラス
まずはステータス用に**Statsクラス**を作ります。
英語だとステータスのことstatsとかstatisticsとか呼ぶみたいですね。言われれば確かにです。

```python:任意のrpyファイル
    class Stats():
        def __init__(self, name, default_value, max_value):
            if default_value > max_value:
                raise ValueError("default_value must be equal to or less than max_value")
            self.name = name
            self.current_value = default_value
            self.max_value = max_value
            self.max_flag = False

        def get_name(self):
            return self.name

        def get_current_value(self):
            return self.current_value

        def add_value(self, diff):
            if not self.max_flag:
                if self.current_value + diff >= self.max_value:
                    self.max_flag = True
                    self.current_value = self.max_value
                else:
                    self.current_value += diff
```
簡単に以下を実装しています。後で他の機能が欲しくなったら追加します。

- 名前、デフォルト値(初期値)、最大値でインスタンス生成
    * キャラクターによってデフォルト値や最大値が違う場合に対応。
- 名前と現在値を取得するメソッド
    * 基本。
    * デフォルト値、最大値、名前はインスタンス生成後変更不可。
- 最大値を超えないように現在値を増やすメソッド
    * レベルアップやステータスアップのアイテムに対応。

味方キャラクターにも敵にも使えるようになっています。敵には`max_value`はいらないかもしれませんが、味方が強くなると敵も強くなるタイプのゲームなら使えそうなので残しておきます。

### PartyMemberクラス
次に味方キャラクター用に**PartyMemberクラス**を作ります。

```python:任意のrpyファイル
    class PartyMember():
        def __init__(self, name, default_stats = {"体力": 100, "攻撃力": 30, "防御力": 15}, max_stats = {"体力": 1000, "攻撃力": 100, "防御力": 100}, skills = None):
            if default_stats.keys() != max_stats.keys():
                raise ValueError("stats keys not corresponding")
            self.name = name
            self.default_stats = default_stats
            self.max_stats = max_stats
            # self.skills = skills
            self.stats = []
            for stats_name in default_stats.keys():
                self.stats.append(Stats(stats_name, default_stats[stats_name], max_stats[stats_name]))

        def get_name(self):
            return self.name

        def get_all_stats(self):
            return self.stats
```
以下を実装しています。スキルについてはあとから必要になると思うので形だけ作り、メソッドは定義していません。

- 名前、デフォルトステータス、最大ステータス、初期スキルを与えてインスタンス生成
    * ここはやや特殊で、自分の作ろうとしているゲームはキャラクターによって持っているステータスの種類が違う（壁は魔力ステータスがないみたいなイメージです）ため、ステータスをdictで渡します。ステータスの種類が共通であれば`default_hp = 100`のような渡し方もできると思います。
    * 渡された値を元に、`stats`リストにStatsクラスのインスタンスを追加します。
- 名前を取得するメソッド
    * 基本。
- ステータスを取得するメソッド
    * `stats`リストを返します。要素はStatsクラスのインスタンスです。

### パーティーメンバーの追加
クラスができたので、パーティーメンバーを追加してみます。

```python:script.rpy
init:
    $party = []
label join_member:
    $party.append(PartyMember(name = "バランス型の人"))
```
これで`join_member`ラベルに飛んだとき、パーティーにデフォルトステータスを持った「バランス型の人」が追加されるようになりました。

## UI実装
ここからはRen'Pyらしいことをやっていきます。
上述のとおり、ヘルプメニューのデフォルト実装を参考に作ってみます。

```python:screens.rpy
screen stats():

    tag menu
    
    python:
        party_member_list = []
        stats_list = []
        for party_member in party:
            party_member_list.append(party_member.get_name())
            stats_dict = {}
            for stats in party_member.get_all_stats():
                stats_dict[stats.get_name()] = stats.get_current_value()
            stats_list.append(stats_dict)

    default member = party_member_list[0]
    default member_stats = stats_list[0] # dict型

    use game_menu(_("ステータス"), scroll="viewport"):

        style_prefix "stats"

        vbox:
            spacing 15

            hbox:

                for i, party_member in enumerate(party_member_list):
                    textbutton _(party_member) action [SetScreenVariable("member", party_member), SetScreenVariable("member_stats", stats_list[i])]
            
            use stats_screen(member, member_stats) 

screen stats_screen(member, member_stats):

    for k, v in member_stats.items():

        hbox:
            label _(k)
            text str(v)
```
以下のような流れになっています。

1. まずPythonコードで各パーティーメンバーに対して名前とステータスのmapを取得する
2. 押されたら`member`変数と`member_stats`変数をセットするテキストボタンを配置する
3. `member`と`member_stats`を渡して画面にステータスを表示する

`member_stats`をRen'Pyコード内で扱えるdict型にしてから渡しているのは、screenに渡されるargsをPythonコード内で使う方法がわからなかったためです……。

ゲームメニューから上で作ったstatsスクリーンを表示できるようにします。メインメニューでは必要ないのでhistoryやsaveと同じところに追加します。

```python:screens.rpy
# 文言をデフォルトからありがちなものに変えています。
# 直近でやっていたエロゲに寄せたなんてことはありません。
screen navigation():

    vbox:
        style_prefix "navigation"

        xpos gui.navigation_xpos
        yalign 0.5

        spacing gui.navigation_spacing

        if main_menu:

            textbutton _("はじめから") action Start()

            textbutton _("つづきから") action ShowMenu("load")

        else:

            textbutton _("ステータス") action ShowMenu("stats") # これを追加

            textbutton _("ログ") action ShowMenu("history")
            ### 後略 ###
```
メニューを開くとまずセーブが開いているのはあまりRPGライクではないので、ステータスを開くようにします。

```python:screens.rpy
################################################################################
## 初期化
################################################################################

init offset = -1
default _game_menu_screen = "stats" # ここを追加
```
これでゲームメニューからパーティーメンバーのステータスを確認できるようになりました。
1人だけのとき：
![SnapCrab_griseo_2020-7-25_23-35-42_No-00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/679910/5a63b6b4-71af-ba97-c43a-d6ecc2a974a2.png)
2人に増えたとき：
![SnapCrab_griseo_2020-7-25_23-36-4_No-00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/679910/f20fce58-65ec-0dfe-8686-8cb5c44af392.png)

# 結論と今後
今回はRen'Pyを使ってパーティーとステータスの管理・確認ができるようになりました。

まだまだバトルができるには程遠いので、道具やスキルなどをゆっくり実装していきたいと思います。