amustallホーム

RPGMakerUniteでアクションRPGを作ろう!!part5

目次

part1
・playerの攻撃を作成(当たり判定なし)までを解説
part2
・playerの常時ステータス表示windowとステータスバー(Hp,MP)までを解説
part3
・敵にHpとGoldを付与して、敵用のHpバーも作成するまでを解説
part4
・敵への攻撃判定までを実装するまでを解説
part5
・playerの攻撃の種類を増やして複数攻撃を実装までを解説
part6
・敵のAIや表示UIの調整から完成まで!!(最終回)

この講座を最後まで呼んでいただけると出来るゲームの内容(このゲームようなアクションRPGをつくります)
ALL YOU NEED IS GOLD 0

複数武器を実装して行こう!!

さて、前回part4までで僕がめっちゃ苦労した部分であり、もしUniteでアクションゲームを 作成したい方がいたら是非読んで頂きたい部分の根幹の部分は終了しています。 前回までで主人公は攻撃できて、はシンボルとしてランダムに徘徊しており、敵から の接触でplayerは設定されたダメージを受け、playerも敵にダメージを与える事ができ、 また敵を撃破すればその報酬として設定したGOLDを取得することができます。
ここまでは出来ていると思いますので、これからは枝葉に近い部分を作りこんで いきましょう。
今回のpart5では、主人公の攻撃の種類を増やしていきます。攻撃用のスクリプトの名前は
My_AttackBaseScritとしています。
何故でしょう?Baseとあるので継承元にするためです。これを継承することで他の攻撃用の スクリプトはとても短く作れます!
また剣撃MP0で使用できますが、攻撃距離を短く変更しましょう。 次に作る火炎遠距離攻撃できますが、 MPを消費する攻撃にしましょう。このように様々な攻撃を準備していけば、 よりゲームが楽しくなりそうですよね!!
それでは早速、part5を初めていきましょう!!

スクリプトの前にアニメーションを作成しよう!

最初の攻撃である剣撃は攻撃自体はspriteを移動させているだけなので、 アニメーションは利用していません。今回つくる攻撃である火炎はspriteのアニメーションを させる事にします。
Assets→Storage→Images→Objectsの中から攻撃に使えそうなspriteを選びましょう。 筆者はFlame_013を使用する事にしました。これを選択した状態でctrl+Dで複製しましょう。

photo

複製できたら、それを適当なfolder(筆者はMy_Spritesフォルダー)に移動して使用しやすい名前に変更しましょう。

photo

さて、それではこのspriteを見てみましょう。以下の様に3枚の絵が連なっていますね。

photo

きっとやり方は沢山あると思いますが、今回はこのspriteを3分割してアニメーションを作成することにしましょう。 スプライトを選択して、スプライトモードを複数にしてから、sprite editorを起動してください。

photo

sprite editorを起動したら、スライスボタンを押して、Grid by Cell Sizeで適切なサイズを指定して 分割します。このスプライトは294*98なので98*98が横に三枚並んでいる事になるので、サイズを98*98に してから、適応ボタンを押してください。すると以下の様に分割されるはずです。

photo
photo

さて分割されたら、スプライトの下準備は完了です。続いていMy_FlameObjectという名前の 空のオブジェクトを作成して、その子オブジェクトに先ほど生成したflame_0を設定してください。

photo

できたら、flame_0を選択して、ウインドウ→アニメーション→アニメーションと選んでください。 すると以下の画面になるので作成を選択してください。

photo

作成を押すと以下の画面になるので適当なファイル名をつけてください。

photo

作成したアニメーションに、スプライトを割り当てて行きましょう。以下の図の様に必要な位置に 必要なスプライトをドラッグアンドドロップしてください。このアニメーションを作成する時は 色々な本で書いてありましたが、初めまりと最後同じアニメーションを入れると綺麗に 見えるとのことなので、ここでもそれに習っています。
これでゲームモードにしてみてください。炎が揺らいでいるはずです。

photo

ここまで出来たら最後に忘れないように以下の図を参考にしてレイヤーを設定してください。それができたら プレハブにしておきましょう。

photo

継承を使ったコードに変更しよう!

まずは、これを書いていて気が付いたエラーがあります。現状攻撃を連射して出していると、 敵が死亡した時に複数回GOLDを獲得できてしまいます。これはUnite側の処理と、スクリプト側の処理の 2つでイベントの操作をしているため起きています。つまり今は死亡時にアニメーションがでて、ウェイトして イベントを移動させていますが、この移動まで間イベントはまだあるので後続の攻撃がhitしてGOLD獲得 処理がスクリプトから呼ばれてしまいます。まずはこのエラーを直します。
直すためにどうしたらいいでしょう?解決方法は色々ありますが、今回は死亡用のフラグを作成して 解決することにします。


//以下改変-----------------------------------------------------------------------------------------------------
public int my_EnemyHp;
public int my_EnemyGold;
//攻撃が死亡後も当たりGOLDを沢山取得できるのでflag管理追加
public bool death;
//----------------------------------------------------------------------------------------------------以下改変
            

上記をCharacterOnMap(ソースコード)に追加してください。このフラグを死亡時に最初に変更 する処理をして、if分で条件分岐させておけばこのエラーは解消できそうですね!

さて、それではコードを改変していきましょう。まずはMy_AtttackBaseScriptを変更します。 このスクリプトを継承して今後の攻撃を実装していきます。例によって全文載せておきます。


using System.Collections;
using System.Collections.Generic;
using UnityEngine;
//MapEventExecutionController
using RPGMaker.Codebase.Runtime.Map;
using RPGMaker.Codebase.CoreSystem.Knowledge.Enum;
//eventonmap
using RPGMaker.Codebase.Runtime.Map.Component.Character;
//runtimesavedatamodel
using RPGMaker.Codebase.CoreSystem.Knowledge.DataModel.Runtime;
//datamanager
using RPGMaker.Codebase.Runtime.Common;

public class My_AttackBaseScript : MonoBehaviour
{
    //継承するので使う項目をpublicにする
    //攻撃のスピード
    public float speed = 4.0f;
    //攻撃時のplauerの向き
    public CharacterMoveDirectionEnum characterDirection;
    //攻撃のx座標
    public int attack_nowX;
    public int attack_nowY;
    //eventMapのリスト
    public List<EventOnMap> eventOnMaps;
    public RuntimeSaveDataModel saveDataModel;
    //playerCanvas
    public GameObject my_PlayerCanvas;
    private My_PlayerCanvasScript my_PlayerCanvasScript;
    // Start is called before the first frame update
    void Start()
    {
       
    }

    // Update is called once per frame
    void Update()
    {
        //継承先でupdateで呼ぶのでここでは呼ばない
        //SordMove();
        //GetEvent();
    }

    //攻撃prefab生成時の共通のinit処理
    public void AttackInit() {
        my_PlayerCanvas = GameObject.FindGameObjectWithTag("My_PlayerCanvas");
        my_PlayerCanvasScript = my_PlayerCanvas.GetComponent<My_PlayerCanvasScript>();
        //生成時にplayerの向きを取得
        characterDirection = MapManager.OperatingCharacter.GetCurrentDirection();
    }

    //向きに応じて攻撃objectを移動
    public void AttackMove() {
        if (characterDirection == CharacterMoveDirectionEnum.Up)
        {
            transform.position = (Vector2) transform.position + new Vector2(0, 1) * speed * Time.deltaTime;
        }
        else if (characterDirection == CharacterMoveDirectionEnum.Down)
        {
            transform.position = (Vector2) transform.position + new Vector2(0, -1) * speed * Time.deltaTime;
        }
        else if (characterDirection == CharacterMoveDirectionEnum.Right)
        {
            transform.position = (Vector2) transform.position + new Vector2(1, 0) * speed * Time.deltaTime;
        }
        else if (characterDirection == CharacterMoveDirectionEnum.Left)
        {
            transform.position = (Vector2) transform.position + new Vector2(-1, 0) * speed * Time.deltaTime;
        }
    }

    //プレイヤーの向きに応じて回転
    public void RotetionChange() {
        
        if (characterDirection == CharacterMoveDirectionEnum.Up)
        {
            transform.Rotate(0, 0, 180);
        }
        else if (characterDirection == CharacterMoveDirectionEnum.Down)
        {
            transform.Rotate(0, 0, 0);
        }
        else if (characterDirection == CharacterMoveDirectionEnum.Right)
        {
            transform.Rotate(0, 0, 90);
        }
        else if (characterDirection == CharacterMoveDirectionEnum.Left)
        {
            transform.Rotate(0, 0, -90);
        }
    }

    
    /// 攻撃オブジェクトの現在地をintで取得する関数X、Y用
    
    public int GetAttackPositionIntX() {
        float attackX = transform.position.x;
        attack_nowX = (int) attackX;
        return attack_nowX;
    }
    public int GetAttackPositionIntY() {
        float attackY = transform.position.y;
        attack_nowY = (int) attackY;
        return attack_nowY;
    }

    //関数GetEvent()は継承先で呼ぶのでここからは消しておきます

    //AttackPoint:攻撃力 effect:攻撃ヒット時のeffectの種類
    public void EnemyProcess(int i, int AttackPoint, string effect) {
        //対象のゲームオブジェクトを取得
        GameObject targetObject = MapEventExecutionController.Instance.GetEventMapGameObject(eventOnMaps[i].MapDataModelEvent.eventId);
        //取得したゲームオブジェクトの子オブジェクトのmy_EnemycanvasScriptを取得
        My_EnemyCanvasScript my_enemyCanvasScript = targetObject.GetComponentInChildren<My_EnemyCanvasScript>();
        
        //hp減算処理
        eventOnMaps[i].my_EnemyHp -= AttackPoint;
        
        //hpスライダーに反映
        my_enemyCanvasScript.enemyHp = eventOnMaps[i].my_EnemyHp;
        //enemyHpスライダーに反映
        my_enemyCanvasScript.setEnemyHp();

        //self switchの取得 
        saveDataModel = DataManager.Self().GetRuntimeSaveDataModel();
        var selfSwitchData = saveDataModel.selfSwitches.Find(selfSwitch => selfSwitch.id == eventOnMaps[i].MapDataModelEvent.eventId);
        //セルフスイッチ変更 Aをoff(敵側の攻撃をさせない) Bをonで攻撃を受ける 
        selfSwitchData.data[0] = false;
        selfSwitchData.data[1] = true;

        //effectの設定 変数2:playerAttackEffect(Uniteの変数は1番目はdata[0])
        saveDataModel.variables.data[1] = effect;
        Debug.Log("attack effect: " + saveDataModel.variables.data[1]);

        //イベント発火
        eventOnMaps[i].ExecuteEvent(MapEventExecutionController.Instance.EndTriggerEvent, false);

        if (eventOnMaps[i].my_EnemyHp > 0)
        {
            //攻撃オブジェクト消去
            Destroy(this.gameObject);
        }
        else if (eventOnMaps[i].my_EnemyHp <= 0)
        {
            //攻撃が死亡時に複数回当たってgoldを沢山手に入れてしまうので最初にフラグ確認
            //ここで先ほど作成したfalgを使用しています
            if(eventOnMaps[i].death == false)
            {
                //敵を倒したときのGOLDを取得する処理
                var party = DataManager.Self().GetGameParty();
                party.GainGold(eventOnMaps[i].my_EnemyGold);
            }
            //GOLDを取得したらすぐに死亡フラグ立てる
            //攻撃が複数回当たるのはイベントのwaitのせいなので先にflag変更
            eventOnMaps[i].death = true;
            //cがonで死亡イベント発火
            selfSwitchData.data[1] = false;
            selfSwitchData.data[2] = true;
            
            //敵のHP表示用のバーを見えなくする
            targetObject.SetActive(false);

            //イベント発火
            eventOnMaps[i].ExecuteEvent(MapEventExecutionController.Instance.EndTriggerEvent, false);
            //playerのcanvasに取得したゴールドを表示
            my_PlayerCanvasScript.SetGold(eventOnMaps[i].my_EnemyGold.ToString());
            
            //攻撃オブジェクト削除
            Destroy(gameObject);
        }
    }
}
            

さてこれで継承元の準備はできました。それでは継承先のコードを作成しましょう。 今までは先ほどのMy_AttackBaseScriptで剣撃の処理をしていましたが、継承元にしたので、 新しく剣撃用にスクリプトを作成します。My_SordAttackScriptという名前でC#スクリプトを作成して 内容を以下にしてください。


using System.Collections;
using System.Collections.Generic;
using UnityEngine;
//MapEventExecutionController
using RPGMaker.Codebase.Runtime.Map;

public class My_SordAttackScript : My_AttackBaseScript
{
    // Start is called before the first frame update
    void Start()
    {
        //継承元で仮に定義しているので、ここで必要な値にする
        speed = 4;
        //武器共通のinit
        AttackInit();
        //武器生成時に向きを考慮する場合に呼ぶ
        RotetionChange();
    }

    // Update is called once per frame
    void Update()
    {
        AttackMove();
        GetEvent();
    }

    public void GetEvent() {
        //全てのマップにあるイベントを取得
        eventOnMaps = MapEventExecutionController.Instance.GetEvents();

        for (int i = 0; i < eventOnMaps.Count; i++)
        {
            //自分の位置と同じ場所にあるイベントを取得
            if (eventOnMaps[i].x_now == GetAttackPositionIntX() && eventOnMaps[i].y_now == GetAttackPositionIntY())
            {
                //第2引数:攻撃力 第三引数:攻撃hit時のeffect
                //noteで敵の種類ごとの処理を分岐
                if (eventOnMaps[i].MapDataModelEvent.note == "gorotuki")
                {
                    EnemyProcess(i, 5, "0");
                }
                else if (eventOnMaps[i].MapDataModelEvent.note == "touzokuM")
                {
                    EnemyProcess(i, 5, "0");
                }
            }
        }
    }
}
            

継承先はこれだけです。今後はこれをコピペして一部改変して使っていけば色んな攻撃が作れます。 今は剣撃はMP0で使用できて射程もかなり長いので、射程は短くしておきましょう。 まずはprefabにしている剣撃用のオブジェクトのMy_AttackBaseScriptを外して、 このMy_SordAttackScriptをアタッチしてください。 そして、My_DestroyAttacjObjectlifetimeを1とかに設定して射程を短くしてください。

新しい攻撃火炎を作成しよう!

もとからあった剣撃のコード改変が出来たので、新しい攻撃を作成してみましょう。といっても 簡単です。もうすでにMy_FlameObjectのプレハブが出来ているので、プレハブに、 My_DestroyAttackObjectをアタッチして、新しくMy_FlameScriptを作成して、 内容を以下にしてアタッチしてください。


using System.Collections;
using System.Collections.Generic;
using UnityEngine;
//MapEventExecutionController
using RPGMaker.Codebase.Runtime.Map;

public class My_FlameScript : My_AttackBaseScript
{
    // Start is called before the first frame update
    void Start() {
        //継承元で仮に定義している
        speed = 5;
        //武器共通のinit
        AttackInit();
        
    }

    // Update is called once per frame
    void Update() {
        AttackMove();
        GetEvent();
    }

    public void GetEvent() {
        //全てのマップにあるイベントを取得
        eventOnMaps = MapEventExecutionController.Instance.GetEvents();

        for (int i = 0; i < eventOnMaps.Count; i++)
        {
            //自分の位置と同じ場所にあるイベントを取得
            if (eventOnMaps[i].x_now == GetAttackPositionIntX() && eventOnMaps[i].y_now == GetAttackPositionIntY())
            {
                //第2引数:攻撃力 第三引数:攻撃hit時のeffect
                //noteで敵の種類ごとの処理を分岐
                if (eventOnMaps[i].MapDataModelEvent.note == "gorotuki")
                {
                    EnemyProcess(i, 10, "1");
                }
                else if (eventOnMaps[i].MapDataModelEvent.note == "touzokuM")
                {
                    EnemyProcess(i, 10, "1");
                }
            }
        }
    }
}
            

コードはこれだけです。殆どコピペですね。今回は生成時にobjectの回転はしなくていいので、 start関数でRotationChangeは呼んでいないのと、攻撃力effectの種類を変えているだけです。 スピードは少し早くして、My_DestroyAttackObjectのlifetimeをとかにして射程を長く してみましょう。そして、攻撃Hit時のeffectを変更できるようにするためにコモンイベントを 変更します。以下を参考にして変更してください。以下の以下で囲っている部分を追加しています。 今後この部分を増やしていけば色んな攻撃毎にeffectを分けて使用することが出来ます。

photo

これで簡単に剣撃とは見た目射程威力hit effectも異なる攻撃ができました。 後は実際に使用するためのボタンの配置、そして火炎は射程は長いがMPを消費する攻撃にするように、 残りを実装していきましょう。

MPを消費する仕組みを実装しよう!

まずはMy_PlayerManagerScriptにMPを消費するためのスクリプトを追加しましょう。 以下の関数を追加してください。


//mp消費
public int ChangePlayerMp(int cost) {
    var party = DataManager.Self().GetGameParty();
    GameActor = party.Actors[0];
    GameActor.Mp -= cost;
    return GameActor.Mp;
}
            

上記関数のcostに消費するMPを引数として渡せばOKです。後は先ほど作成したプレハブを参照して、 実際に攻撃できるようにMy_PlayerMangaerScriptを改変します。こちらも分かりやすいように 全部載せておきます!


using System.Collections;
using System.Collections.Generic;
using UnityEngine;
//mapmanager
using RPGMaker.Codebase.Runtime.Map;
//datamanager
using RPGMaker.Codebase.Runtime.Common;
//gameactor
using RPGMaker.Codebase.Runtime.Battle.Objects;

public class My_PlayerManagerScript : MonoBehaviour
{
    public GameActor GameActor { get; private set; }
    //playerのstatusの変数
    public int hp;
    public int maxHp;
    public int mp;
    public int maxMp;
    public int gold;

    //攻撃用のプレハブ
    public GameObject sordPrefabObject;
    public GameObject flamePrefabObject;

    //MainCamera
    private GameObject mainCamera;
    //player
    private GameObject player;

    // Start is called before the first frame update
    void Start()
    {
        
    }

    // Update is called once per frame
    void Update()
    {
        
    }

    public void SordAttack() {
        //Debug.Log( MapManager.OperatingCharacter.GetCurrentPositionOnTile());
        //Debug.Log(MapManager.OperatingCharacter.GetCurrentDirection());
        Instantiate(sordPrefabObject, MapManager.OperatingCharacter.GetCurrentPositionOnTile(), Quaternion.identity);
    }

    public void FlameAttack() {
        //MPが8無いとflameは使用出来ない
        int flameAttackCost = 8;
        if (GetPlayerMp() < flameAttackCost)
        {
            return;
        }
        else
        {
            ChangePlayerMp(flameAttackCost);
            Instantiate(flamePrefabObject, MapManager.OperatingCharacter.GetCurrentPositionOnTile(), Quaternion.identity);
        }
    }

    /// playerキャラクターのhpを取得してint型で返す変数
    /// Actors[0]で一番最初のキャラクターを指定
    public int GetPlayerHp() {

        //Data,anagerはstaticなので直接参照できる
        var party = DataManager.Self().GetGameParty();
        GameActor = party.Actors[0];
        hp = GameActor.Hp;
        return hp;
    }
    public int GetPlayerMaxHp() {
        var party = DataManager.Self().GetGameParty();
        GameActor = party.Actors[0];
        maxHp = GameActor.Mhp;
        return maxHp;
    }
    //MP取得
    public int GetPlayerMp() {
        var party = DataManager.Self().GetGameParty();
        GameActor = party.Actors[0];
        mp = GameActor.Mp;
        return mp;
    }
    //mp消費
    public int ChangePlayerMp(int cost) {
        var party = DataManager.Self().GetGameParty();
        GameActor = party.Actors[0];
        GameActor.Mp -= cost;
        return GameActor.Mp;
    }
    public int GetPlayerMaxMp() {
        var party = DataManager.Self().GetGameParty();
        GameActor = party.Actors[0];
        maxMp = GameActor.Mmp;
        return maxMp;
    }
    //Gold取得
    public int GetPlayerGold() {
        var party = DataManager.Self().GetGameParty();
        gold = party.Gold;
        return gold;
    }

    //playerのGameObject参照
    public GameObject GetPlayerObject() {
        mainCamera = GameObject.FindGameObjectWithTag("MainCamera");
        player = mainCamera.transform.parent.gameObject;
        return player;
    }
}
            

MP消費に関してはFlameAttack関数で処理しています。後はこの関数を火炎ボタンを押した時に 呼ばれるように設定すればいいですね!あとMy_FlameObjectを忘れずにインスペクターからアタッチしておきましょう。
以下のようにAttackButtonをコピーしてFlameAttackButtonを作成しましょう。

photo

そしてこのボタンのクリックイベント用にMy_SecenMapCanvasScriptに以下のスクリプトを追加してください。


//火炎ボタン
public void ClickFlameAttackButton() {
    my_PlayerManagerScript.FlameAttack();
}
            

さて、これで火炎ボタンを押してみてください!無事に火炎の攻撃が発射され、 敵にもダメージを与えて、ちゃんとMPも消費されているはずです!!
これで無事今回のpart5は終了です!次回のpart6で敵AIを作成していきます!! ここまで読んで頂いてありがとうございました!!

ブログ一覧へ戻る