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で複製しましょう。
複製できたら、それを適当なfolder(筆者はMy_Spritesフォルダー)に移動して使用しやすい名前に変更しましょう。
さて、それではこのspriteを見てみましょう。以下の様に3枚の絵が連なっていますね。
きっとやり方は沢山あると思いますが、今回はこのspriteを3分割してアニメーションを作成することにしましょう。 スプライトを選択して、スプライトモードを複数にしてから、sprite editorを起動してください。
sprite editorを起動したら、スライスボタンを押して、Grid by Cell Sizeで適切なサイズを指定して 分割します。このスプライトは294*98なので98*98が横に三枚並んでいる事になるので、サイズを98*98に してから、適応ボタンを押してください。すると以下の様に分割されるはずです。
さて分割されたら、スプライトの下準備は完了です。続いていMy_FlameObjectという名前の 空のオブジェクトを作成して、その子オブジェクトに先ほど生成したflame_0を設定してください。
できたら、flame_0を選択して、ウインドウ→アニメーション→アニメーションと選んでください。 すると以下の画面になるので作成を選択してください。
作成を押すと以下の画面になるので適当なファイル名をつけてください。
作成したアニメーションに、スプライトを割り当てて行きましょう。以下の図の様に必要な位置に
必要なスプライトをドラッグアンドドロップしてください。このアニメーションを作成する時は
色々な本で書いてありましたが、初めまりと最後に同じアニメーションを入れると綺麗に
見えるとのことなので、ここでもそれに習っています。
これでゲームモードにしてみてください。炎が揺らいでいるはずです。
ここまで出来たら最後に忘れないように以下の図を参考にしてレイヤーを設定してください。それができたら プレハブにしておきましょう。
まずは、これを書いていて気が付いたエラーがあります。現状攻撃を連射して出していると、
敵が死亡した時に複数回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_DestroyAttacjObjectのlifetimeを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を5とかにして射程を長く してみましょう。そして、攻撃Hit時のeffectを変更できるようにするためにコモンイベントを 変更します。以下を参考にして変更してください。以下の以下で囲っている部分を追加しています。 今後この部分を増やしていけば色んな攻撃毎にeffectを分けて使用することが出来ます。
これで簡単に剣撃とは見た目も射程も威力もhit effectも異なる攻撃ができました。 後は実際に使用するためのボタンの配置、そして火炎は射程は長いが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を作成しましょう。
そしてこのボタンのクリックイベント用にMy_SecenMapCanvasScriptに以下のスクリプトを追加してください。
//火炎ボタン
public void ClickFlameAttackButton() {
my_PlayerManagerScript.FlameAttack();
}
さて、これで火炎ボタンを押してみてください!無事に火炎の攻撃が発射され、
敵にもダメージを与えて、ちゃんとMPも消費されているはずです!!
これで無事今回のpart5は終了です!次回のpart6で敵AIを作成していきます!!
ここまで読んで頂いてありがとうございました!!