ゲーム化!tomo_manaのブログ

ゲーム化!tomo-manaのブログ

Unityでゲームを作る方法について紹介しています

Unity学習#23 (Unity 2019.4.1f1) ステータスとアイテム効果の実装

今回は、アイテムを使った時の、特にプレイヤーキャラクターのステータスに与える効果を実装していきます。また、キャラクターのステータスも未実装でしたので、併せて実装します。
今回から、少し記事の単位を小さくします。最近は、設計が進んできたために、少し修正するだけでコードの掲載量が多くなり、結果的に記事が読みにくくなってきたためです。また、Unityの基本的な画面操作も慣れてきたので、細かい説明は省略していきます。

<目次>


今回の作業は、全体的に第18回で作成したアイテムとステータスの構造図を参考にしながら進めます。
tomo-mana.hatenablog.com

ステータスとアイテム効果の関係

アイテム効果は、ステータスに影響を与えるものだけに限定して言うと、ステータスの構造に依存します。そのため、ステータスを構成するパラメータに増減がある度に、アイテム効果にも変更が入ることになります。

「アイテムの効果」自体は、その他にもゲームのあらゆる場面に作用するものがあるはずです。ゲームシステムのフラグ変更、敵とのエンカウント率、戦闘(駆け引き)に関わるもの、その他ゲームバランスを整える様々なアドバンテージやペナルティに用いられます。ゲームシステムについて、新しい発想が生まれるたびに、アイテム効果に新しいパラメータが生まれます。必要なものは、今後必要に応じて実装します。今回はアイテム効果という言葉はステータスへの影響に限定します。

ステータスの実装

ステータスはキャラクターに依存します。キャラクターはパーティーに含まれます。これらを図にすると以下のようになります。

f:id:tomo_mana:20201213132346p:plain

パーティー

パーティ―はキャラクターを追加・削除可能な配列として持ちます。これをコードにすると以下になります。

Party.cs

using System.Collections;
using System.Collections.Generic;

public class Party
{
    // パーティメンバーの一覧
    public List<PartyCharacter> member;
    
    public Party()
    {
        member = new List<PartyCharacter>();
    }
}

キャラクター

キャラクターはステータスを持ちます。ステータスを直接触れるようにするかについては賛否あると思いますが、今はステータスに直接触れるようにします。キャラクターは必ず何らかのステータス値を初期値として持つように初期化します。

PartyCharacter.cs

public class PartyCharacter
{
    public PartyCharacterStatus status;
    
    public PartyCharacter( ushort life, ushort lifeMax, byte attack, byte speed )
    {
        status = new PartyCharacterStatus();
        status.life = life;
        status.lifeMax = lifeMax;
        status.attack = attack;
        status.speed = speed;
    }
}

ステータス

ステータスは第18章で定義した通り、生命力(削る対象)、攻撃力(削る力)、速度(削る速さ)の3つを定義します。また、削る対象は最大値を併せて定義します。この後アイテム効果を定義するため、アイテム効果がどのアイテムへの効果を記述したものか分かるように、ステータスの各値を表す列挙体を定義します。

PartyCharacterStatus.cs

public enum CharacterStatusParams
{
    LIFE = 0,   // 生命力(削る対象)
    LIFEMAX,    // 生命力の最大値
    ATTACK,     // 攻撃力(削る力)
    SPEED,       // 速度(削る速さ)
    NUMBER_OF_PARAMS
};

public class PartyCharacterStatus
{
    public ushort life;     // 生命力(削る対象)
    public ushort lifeMax;  // 生命力の最大値
    public byte attack;    // 攻撃力(削る力)
    public byte speed;     // 速度(削る速さ)
}

※PartyCharacterStatus は、今は一つ一つを独立した変数として定義していますが、次回行う最大最小判定の中で、列挙体を使ってアクセスできるコレクション型のリスト配列に変更するかもしれません。

アイテム効果の実装

次に、アイテム効果を定義します。アイテム効果は、先ほど定義したステータス名の列挙体(どのステータスに効果を与えるか)と、その効果(値)とを定義します。アイテム効果は、今は外からアクセスできるようにしていますが、辞書として使うなら変更不可にしないといけないはずですので、後々Visitorパターンなどで置き換えるかもしれません。

アイテム効果の英名ですが、本来はItemEffectなどが分かりやすくて良さそうと思ったのですが、Effect はアイテムを使った時のグラフィックなどにも使われそうな言葉なので、薬の効能の意味を持つ Efficacy にしました。あまり聞きなれない言葉ですが、薬を使った時に、どの部位にどのように働く、といった説明に使う単語のようです。

Efficacy.cs

public class Efficacy
{
    // 対象となるステータス
    public CharacterStatusParams status;
    // 値
    public ushort value;
    
    public Efficacy( CharacterStatusParams status, ushort value )
    {
        this.status = status;
        this.value = value;
    }
}

誰がパーティーを管理するか

パーティー情報は、アイテム一覧と同じく、本来はセーブデータに属します。しかし、今はシーンも一つしかなく、セーブデータも定義していませんので、メニューから操作できるように、メニューの直下にぶら下げます。

構造図(イメージ図)

少しステータス関係は構造をきっちりと分けたので、今後、セーブデータや辞書などのグループに分けた時に、できるだけ簡単に移動できるように、今はメニューの一部としてぶら下げておきます。

f:id:tomo_mana:20201213221859p:plain
ステータスとアイテム効果をどこにぶら下げるか

コード

メニューからステータス、アイテム効果への参照

MenuContext.cs

public class MenuContext
{
    // アイテム情報
    public ItemContext itemContext;
    // パーティ情報
    public Party party;    // 追加
}
ステータスとアイテム効果の初期化

(1) 先ほどのクラス図には出てきませんが、MenuContext自体を初期化するUI管理クラスで、パーティへのメンバー追加とステータスの初期化を入れます。

InputUIManager.cs

public class InputUIManager : MonoBehaviour
{
    // 中略
    
    // Start is called before the first frame update
    void Start()
    {
        // 中略
        
        // メニューコンテキストの初期化
        context = new MenuContext();
        context.itemContext = new ItemContext();
        
        // パーティの初期化
        context.party = new Party();    // 追加
        // キャラを1名追加しておく
        context.party.member.Add(
            new PartyCharacter(
                60,      // Life
                100,    // LifeMax
                10,      // Attack
                5         // Speed
            )
        );
    }
}

(2) アイテム効果は、とりあえずこれまでアイテムメニュー内で初期化していましたので、今回は以前の修正を踏襲します。

ItemContext.cs

public class ItemContext
{
    // 中略
    
    public ItemContext()
    {
        itembag = new List<Item>();
        
        // 回復テストにアイテム効果を追加
        itembag.Add(
            new Item(
                "Bread", 
                DISPOSABLE | USABLE, 
                new Efficacy( CharacterStatusParams.LIFE, 20 )    // 追加
            )
        );  // 回復テスト
        
        // 以下略
    }
}

アイテムを使った時の処理

第22回で実装したアイテム操作を決定した処理内で、アイテム効果をステータスに反映する処理を実装します。

修正コード

QueryUseWindow.cs

public class QueryUseWindow : Window, ItemPrefabAdapter, ISubmitHandler
{
    // 中略
    
    public void OnSubmit(BaseEventData eventData)
    {
        // 選択番号→アクション(前回と同じ)
        int i, c = 0;
        int s = context.itemContext.itemId;
        int no = context.itemContext.itemOperation;
        for(i = 0; i < 3; i++){
            if( (context.itemContext.itembag[s].usage & (1 << i)) != 0 ){
                if( c == no ){
                    context.itemContext.itemOperation = i;
                    break;
                } else {
                    c++;
                }
            }
        }
        // アイテムを使った!表示
        string str = context.itemContext.itemOperationString[ context.itemContext.itemOperation ] +
            " " +
            context.itemContext.itembag[ context.itemContext.itemId ].name;
        Debug.Log(gameObject + ".OnSubmit: " + str);
        if( textWindow != null ){
            textWindow.Write(str);
        }
        // アイテムの作用と表示
        if( context.itemContext.itemOperation == (int)ItemUsage.USE ){
            Efficacy efficacy = context.itemContext.itembag[ context.itemContext.itemId ].efficacy;
            if( efficacy != null ){
                if( efficacy.value != 0 ){
                    
                    PartyCharacterStatus status = context.party.member[0].status;
                    string strEfficacyStatus = null;
                    
                    if( efficacy.status == CharacterStatusParams.LIFE ){
                        
                        // 体力変化フラグ
                        // 0: 回復または減少
                        // 1: 何も起きない(MAX)
                        // 2: 気絶(MIN)
                        int lifeChanged = 0;
                        int lifeChangedValue = 0;
                        
                        // 作用
                        Debug.Log( "before use item life is :" + status.life + "/" + status.lifeMax );
                        if( (efficacy.value > 0) && (status.life == status.lifeMax) ){
                            lifeChanged = 1;
                        } else
                        if( (efficacy.value < 0) && (status.life < -(efficacy.value)) ){
                            lifeChanged = 2;
                            lifeChangedValue = -((int)status.life); // 生命の残り分だけ減った
                            status.life = 0;
                        } else {
                            if( status.life + efficacy.value > status.lifeMax ){
                                lifeChangedValue = status.lifeMax - status.life;
                            } else {
                                lifeChangedValue = efficacy.value;
                            }
                            status.life = (ushort)((int)status.life + lifeChangedValue);
                        }
                        Debug.Log( "after  use item life is :" + status.life + "/" + status.lifeMax );
                        
                        // 表示
                        switch( lifeChanged ){
                        case 0:
                            if( efficacy.value > 0 ){
                                strEfficacyStatus = "Life is cured: " + lifeChangedValue;
                            } else {
                                strEfficacyStatus = "Life is decreased: " + -lifeChangedValue;
                            }
                            break;
                        case 1:
                            strEfficacyStatus = "Nothing Happened";
                            break;
                        case 2:
                            strEfficacyStatus = "Life is decreased: " + -lifeChangedValue + "... Life is expired";
                            break;
                        default:
                            break;
                        }
                    } else {
                        // 対象のパラメータ(パラメータを配列にすればswitch文は不要)
                        switch( efficacy.status ){
                        case CharacterStatusParams.LIFEMAX:
                            strEfficacyStatus = "LifeMax is ";
                            status.lifeMax = (ushort)((int)status.lifeMax + efficacy.value);
                            break;
                        case CharacterStatusParams.ATTACK:
                            strEfficacyStatus = "Attack is ";
                            status.attack += (byte)efficacy.value;
                            break;
                        case CharacterStatusParams.SPEED:
                            strEfficacyStatus = "Speed is ";
                            status.speed += (byte)efficacy.value;
                            break;
                        }
                        
                        // パラメータへの永久的な効果
                        if( efficacy.value > 0 ){
                            strEfficacyStatus = strEfficacyStatus + "increased: ";
                        } else {
                            strEfficacyStatus = strEfficacyStatus + "decreased: ";
                        }
                        strEfficacyStatus = strEfficacyStatus + efficacy.value;
                    }
                    Debug.Log(strEfficacyStatus);
                }
            } else {
                Debug.Log("Nothing Happened");
            }
        }
        // 以下 略
    }
}

とりあえず今回はアイテム効果を参照して、ステータスに何らかの影響を与えるところまで実装しました。ただしこのままだと、オーバーフローが発生してしまうので、最大・最小チェックを追加します。

次回やること

ステータス変更の最大最小チェック:
●各ステータスの最大・最小値の決定
●アイテム効果をステータスの最大・最小値でクリップする