part1
・playerの攻撃を作成(当たり判定なし)までを解説
part2
・playerの常時ステータス表示windowとステータスバー(Hp,MP)までを解説
part3
・敵にHpとGoldを付与して、敵用のHpバーも作成するまでを解説
part4
・敵への攻撃判定までを実装するまでを解説
part5
・playerの攻撃の種類を増やして複数攻撃を実装までを解説
part6
・敵のAIや表示UIの調整から完成まで!!(最終回)
この講座を最後まで呼んでいただけると出来るゲームの内容(このゲームようなアクションRPGをつくります)
⇒ALL YOU NEED IS GOLD 0
さてpart3までで、仮の攻撃、playerと敵のステータスを作成してきました。そしてついに このpart4で実際の攻撃判定を作成していきます。いままでの中では一番難しい部分かもしれないので 丁寧に解説していきます。このpart4までクリアできればアクションRGPの大まかな骨組みはだいたい 出来たと思います!!それではさっそく作っていきましょう!
敵は今はランダムに動くのみで、攻撃も何もできません、この敵の動きや攻撃がhitしたときの effect、敵を倒したときのeffectや倒した後の処理はUnite側のコモンイベントを利用して作成します。 コモンイベントで作成しておくと同様の敵を作成するときにコピペできるので便利です。 そして、コモンイベントではセルフスイッチを使用して敵の攻撃できる状態や攻撃を受けている状態等を 場合分けしているのでまずはセルフスイッチを初期化する処理をMy_EventManagerスクリプトの EnemyEventInitProcess()に記載します。追加後のコードを以下に示します。
public void EnemyEventInitProcess(int i, int hp, int enemyGold) {
//Hp設定
eventOnMaps[i].my_EnemyHp = hp;
//Gold設定
eventOnMaps[i].my_EnemyGold = enemyGold;
//self switchの取得 初期化もしている
var saveDataModel = DataManager.Self().GetRuntimeSaveDataModel();
var selfSwitchData = saveDataModel.selfSwitches.Find(selfSwitch => selfSwitch.id == eventOnMaps[i].MapDataModelEvent.eventId);
if (selfSwitchData == null)
{
selfSwitchData = new RuntimeSaveDataModel.SaveDataSelfSwitchesData();
selfSwitchData.id = eventOnMaps[i].MapDataModelEvent.eventId;
selfSwitchData.data = new List<bool>() { false, false, false, false };
saveDataModel.selfSwitches.Add(selfSwitchData);
}
//セルフスイッチAをtrueにしておく、Aがtureで敵が攻撃
selfSwitchData.data[0] = true;
selfSwitchData.data[1] = false;
selfSwitchData.data[2] = false;
//ターゲットのEVENTのgameObjectをイベントIDから取得
GameObject targetObject = MapEventExecutionController.Instance.GetEventMapGameObject(eventOnMaps[i].MapDataModelEvent.eventId);
//取得したゲームオブジェクトの子オブジェクトにプレハブのenemycanvasを入れる
Canvas enemyCanvasPrefub = Instantiate(my_EnemyCanvas);
enemyCanvasPrefub.transform.SetParent(targetObject.transform, false);
//enemyのHPの初期化 my_EnemyCanvasScriptはそれぞれ必要なのでその分用意
My_EnemyCanvasScript my_EnemyCanvasScript = enemyCanvasPrefub.GetComponent<My_EnemyCanvasScript$gt;();
my_EnemyCanvasScript.enemyMaxHp = eventOnMaps[i].my_EnemyHp;
my_EnemyCanvasScript.enemyHp = my_EnemyCanvasScript.enemyMaxHp;
//sliderも初期化
my_EnemyCanvasScript.InitEnemyHpSlider();
}
上記でセルフスイッチの使用が出来るようになりました。初期化の段階でセルフスイッチAをONにして、 その他B,CはOffにしています。セルフスイッチはDまでありますが、今回はDは使用していません。 続いて、コードの前に最近コモンイベントを作成していきましょう。コモンイベントを以下の内容で 作成してください。
出来たところで解説していきながら、必要なUniteの設定をしていきましょう。
まずはコモンイベントのスイッチの部分を設定しておきましょう。スイッチはなにも指定していなければ
1番になっていますが、この1番のスイッチを意図せずの他の部分で設定してしまうとコモンイベントが
誤作動してしまうので、以下のようにわかり安く設定しておきましょう。
上記が出来たら、コモンイベントで使用している変数を設定しておきましょう。自分は以下のように設定しています。
この変数の名前は特に分かりやすければなんでもいいです。この変数は後述しますがscriptから代入参照ができます。 その時プログラムの世界では1番目が0であることが多いですが、この変数は1番目は1で指定できます。 もう一つ、この変数に直接代入できるのはstring型の様です、int等いれると変換不能のエラーが表示されます。
これで下準備はできたので、コモンイベントの内容を説明していきましょう。
このコモンイベントはa,b,cの3つのセルフスイッチによって挙動が分かれています。
・セルフスイッチAがonの時
このスイッチAはEnemyEventInitProcess(int i, int hp, int enemyGold)関数の中の初期化でまず、
Onにされております。このスイッチAがonの時はイベントはトリガーがイベントから接触なので、イベントから
playerに接触したときにスイッチAの中身が実行されます。中身はみたまんまですが、まずはenemyAttack変数に
30-60のランダムな敵の攻撃力を設定します。(つまり敵の攻撃力は他のHpやGoldとは異なりUnite側の
コモンイベントから個別に設定しています)そしてここで決定した分をpkayerのHpから減少されています。
そして最後に画面を赤にフラッシュされてダメージエフェクトを出して1秒のwaitをいれて(これがないと
敵と接触したときにplayerが即死してします)終了です。つまりこのスイッチAは敵の攻撃を可能にするスイッチです。
・セルフスイッチBがonの時
スイッチBは敵が攻撃を受ける事が出来るようにするスイッチです。敵のHpの減算処理は後述するスクリプト側
で行っています。ここでは敵が攻撃を受けた時のエフェクトを決定して、そのエフェクトを発生させています。
このエフェクトは現在はplayerAttackEffect = 0の時で分岐されています。ここは本来プログラム的には ==
が使われべきですね。この = 0 は0を代入しているわけではなく、0の時という意味です。
そして0の代入はスクリプト側で行っています。さてこのplayerAttackEffectは現状攻撃が1種類であれば
不要ですが、アクションRPGで攻撃が一つではつまらないので今後攻撃をふやせるように、前もって分岐できる
ように設定しています。今回はまだつくりませんが、これ以降のpartで複数攻撃も実装していきます!
・セルフスイッチCがonの時
このスイッチCがonになるのは、敵のHpが0になった時、つまり死亡イベント用のスイッチです。
まずは敵が死亡した時のeffectをアニメーションを利用して発生させています。その後にイベントを移動させる
場所の座標を(9999,9999)に設定してからイベントを移動させて、その後にイベントの一時削除をしています。
イベントの一時削除のみでいいとおもうかもしれませんが、一時削除してもその座標にはイベントが残っているので、
playerの攻撃が当たってしまいます。イベントのグラフィックは見えないのにplayerの攻撃が当たるバグになってしまうので
画面の遥かかなた移動させているのです。
さて上記でコモンイベントの中身の説明は終了です。
これでコモンイベントは作れたので、実際のイベントにこのイベントを設定しましょう。
下の画像で説明すると、マップ設定の中にある、敵がいるマップを選んで、以前作成した敵(画面の例ではenemy)
のイベントを選択して
イベントの実行内容で右クリック→イベントの新規作成→フロー制御→コモンインベント
として選んでください。
ここで今作成したコモンイベントを選べば設定できます。今の図ではenmey2もいるので、このenemy2でもそうですが、
違う種類の敵であれば、その敵様にコモンイベントを新しく作成してください。コモンイベント側で変更するのは攻撃力の
部分のみです、なので新しいコモンイベントはイベント内容のコピペをして攻撃力を変更するだけで作れます。
後はイベントのメモ欄も変更しておきましょう。今回提示しているscriptではtouzokuMとして2種類目の
敵を作成しているので、各自自分で作った識別用の文字列を設定してください。敵の種類を同じにするなら同じコモンイベントを
使い回せはいいですね!
さてコモンイベントは準備できたので、My_AttackBaseScriptを改変していきましょう! 変更部分のみ載せても分かりにくいと思うので変更後のMy_AttackBasescriptを全て載せておきます。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
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
{
//攻撃のスピード
private float speed = 4.0f;
//攻撃時のplauerの向き
private CharacterMoveDirectionEnum characterDirection;
//以下追加分--------------------------------------------
//攻撃のx座標とY座標用の変数
private int attack_nowX;
private 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()
{
//以下追加分-------------------------------------------------------------------
//playerがGoldを取得したときにplayerCanvasに取得GOLDを表示するために取得
my_PlayerCanvas = GameObject.FindGameObjectWithTag("My_PlayerCanvas");
my_PlayerCanvasScript = my_PlayerCanvas.GetComponent<My_PlayerCanvasScript>();
//-----------------------------------------------------------------------------
//生成時にplayerの向きを取得
characterDirection = MapManager.OperatingCharacter.GetCurrentDirection();
//向きに応じてスプライトを回転
RotetionChange();
}
// Update is called once per frame
void Update()
{
SordMove();
//新しく作成した関数をコール
GetEvent();
}
//向きに応じてsordを移動
public void SordMove() {
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;
}
}
//プレイヤーの向きに応じて回転
private 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;
}
//上記同様Y用
public int GetAttackPositionIntY() {
float attackY = transform.position.y;
attack_nowY = (int) attackY;
return attack_nowY;
}
//-----------------------------------------------------
//以下追加分----------------------------------------------
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())
{
//Debug.Log("seach for xy: " + eventOnMaps[i].MapDataModelEvent.name);
//noteで敵の種類ごとの処理を分岐
if (eventOnMaps[i].MapDataModelEvent.note == "gorotuki")
{
//引数の説明:1つ目はforを回すi、二つ目は敵に与えるダメージ、三つめは敵に攻撃あたったときのエフェクトの種類
//Unite側の変数はsritngで与える必要があるのでstringにしている
EnemyProcess(i, 5, "0");
}
else if (eventOnMaps[i].MapDataModelEvent.note == "touzokuM")
{
EnemyProcess(i, 5, "0");
}
}
}
}
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;
//イベント発火・・このイベント発火を呼ばないとトリガーはイベントから接触のため
//上記以外ではイベントは起こらないのでスクリプトからイベントの発火をしておく
eventOnMaps[i].ExecuteEvent(MapEventExecutionController.Instance.EndTriggerEvent, false);
if (eventOnMaps[i].my_EnemyHp > 0)
{
//攻撃オブジェクト消去
Destroy(this.gameObject);
}
//攻撃の後にHpが0以下ならセルフスイッチCをOnにして死亡イベント開始
else if (eventOnMaps[i].my_EnemyHp <= 0)
{
//cがonで死亡イベント発火
selfSwitchData.data[1] = false;
selfSwitchData.data[2] = true;
//Debug.Log("a" + selfSwitchData.data[0]);
//Debug.Log("b" + selfSwitchData.data[1]);
//Debug.Log("c" + selfSwitchData.data[2]);
//敵のHP表示用のバーを見えなくする
targetObject.SetActive(false);
//イベント発火
eventOnMaps[i].ExecuteEvent(MapEventExecutionController.Instance.EndTriggerEvent, false);
//playerのcanvasに取得したゴールドを表示
my_PlayerCanvasScript.SetGold(eventOnMaps[i].my_EnemyGold.ToString());
//敵を倒したときのGOLDを取得する処理
var party = DataManager.Self().GetGameParty();
//Goldを加算
party.GainGold(eventOnMaps[i].my_EnemyGold);
//攻撃オブジェクト削除
Destroy(gameObject);
}
}
}
かなり長くなりましたが、上記です。変更や追加箇所、またスクリプトには出来るだけコメントを
記載しましたので、細かい解説はそちらに任せます。
少し雑談ですが、上記でとっても苦労したのは何気ない一文ですが以下ですね
eventOnMaps[i].ExecuteEvent(MapEventExecutionController.Instance.EndTriggerEvent, false);
この選択されたイベントを任意のタイミングでスクリプトから発火させるコードを見つけるのに、 実はとんでもなく苦戦しました。このアクションRPGを作成し始めた時から、イベントをスクリプトから 発火させる方法がkeyになるだろうと思っていたので、実はこの簡単な一文に他ぼり着くまでに、 結構苦労しました。ソース多分全部では万単位行分あるので、めっちゃ大変でした。 一番初めは、Unite側でアイテムを作成することができるので、そのアイテムにコモンイベントをつけて、 アイテムを使用するスクリプトはソースからアイテム関連をみていたら見つけたので、消費しない 隠しアイテムをplayerに持たせて、必要時にスクリプトからアイテムを使用して、目的のコモンイベントを 呼んでました。この方法で行けるとおもってゲームをつくっていたんですが、アイテムを使用すると Uniteの使用なのかメッセージ表示がでてしまいます。このメッセージはクリックしないと消えません。 (ソースの改変ができるので自動で消す方法も、あるはずですがわかりませんでした><。) なので、例えばplayerがダメージを受ける度にアイテム消費イベントでメッセージボックスが出現して、 それをクリックして消していてはゲーム性がまったく損なわれてしまいます。この問題につきあたって、 単純にスクリプトから呼ぶ方法をさがしていたのですが、まぁ本当にこの方法見つけるまでソースと 6時間はにらめっこしていました。途中で何回もあきらめそうになったので、個人的にこのコードが一番今回の開発で 印象にのこっています。
さて大分脱線しましたが、playerCanvasに現状では取得Goldを表示するtextはないし、そのスクリプト も用意されていないので、そちらの開発に移りましょう!
さてまずは、playerCanvasにGoldを表示するためのTextを追加しましょう。正確には Text mesh proを配置しましょう。以下の図を参考にUIを設定してください。Textのフォント もせっかくなのでUniteのフォントを使用しています。色や大きさ等は各自でいい感じに 調整してください!
上記が出来たらMy_player_CanvasScriptを以下の様に改変してください。いつものように全コード載せておきます。 またコメントにコードの説明も併記しておきます!
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
//Text mesh Pro使うために宣言
using TMPro;
public class My_PlayerCanvasScript : MonoBehaviour
{
//スライダーオブジェクト
public Slider hpSlider;
public Slider mpSlider;
private Canvas playerCanvas;
//playerHP関連
public int playerMaxHp;
public int playerHp;
//playerMp関連
public int playerMaxMp;
public int playerMp;
//Gold表示 ここにインスペクターから先ほど作成したGoldTextをアタッチしてください
public TextMeshProUGUI goldText;
// Start is called before the first frame update
void Start()
{
//goldText初期化
goldText.text = "";
}
// Update is called once per frame
void Update()
{
}
//ステータススライダーの初期化
public void InitPlayerSlider() {
playerCanvas = gameObject.GetComponent<Canvas>();
playerCanvas.worldCamera = Camera.main;
//hp設定
hpSlider.maxValue = playerMaxHp;
hpSlider.value = playerHp;
//mp設定
mpSlider.maxValue = playerMaxMp;
mpSlider.value = playerMp;
}
//ステータススライダーの更新
public void setPlayerSlider() {
hpSlider.maxValue = playerMaxHp;
hpSlider.value = playerHp;
mpSlider.maxValue = playerMaxMp;
mpSlider.value = playerMp;
}
public void SetGold(string gold) {
//初期化
goldText.text = "";
//gold表示
goldText.text = "+" + gold;
//消去
StartCoroutine(EraseGold());
}
//表示後3秒で削除するためにコルーチン処理
IEnumerator EraseGold() {
yield return new WaitForSeconds(3.0f);
goldText.text = "";
}
}
さて、これでゲームを起動してみてください。下記の様に敵との戦闘がついにたのしめるように なってりると思われます。敵を倒したときのGOLD表示もあり、メニューからGoldがちゃんと増えている 事も確認してみてください。
上記のように無事に敵を倒せるになればpart4は無事終了となります。ここまで読んでいただき本当に
ありがとうございました。ここまでもし一緒にUniteで作成しているかたがもしいれば、ついにアクションPRG
らしき物ができたと、すくなからず達成感もあるのではないでしょうか?
ここまでできれば、本講座の骨組みはできたと思います。このあとは今のゲームに肉付けをしていくような
作業になると思います。次回part5では今のところ1種類しかない攻撃を増やしていきます。次の攻撃は
炎を想定しているのでuniteのスプライトを利用して炎を揺らぎながら飛ぶようにアニメーションの設定
もしていきます。そしてpart6では今は単純にランダム動いて、移動持にplayerにぶつかったら攻撃処理
が起こるだけですので、普段はランダムに動いてるがplayerが一定距離にちかずくとplayerを追跡、
一定距離以上離れたらまたランダムに戻るという敵のAIを作成する予定です!!
今後の更新も是非読んでみてください!!