Unity:階移動(ローグライクを自分なりの解釈で作ってみる)
概要
- シーンを切り替えずに階を移動したい.
完成図
レイヤーを切り替える
配列を全く新しいものに切り替えてあげます.もちろんその際過去に前レイヤーのオブジェクトは全て削除します.
削除する際にわざわざ全てのオブジェクトを検索して削除するのは面倒です.なので空オブジェクトの子要素としてオブジェクトを生成し, 削除する際は「空オブジェクトの子要素全て削除!」で済ませるようにします.
qiita.com 上記の記事から引用
public void DeleteStage(){ foreach (Transform n in ParentObj.transform) { Destroy(n.gameObject); } }
もちろんオブジェクトを生成する際は空オブジェクトの子要素に追加していきます
public void CreateStage(){ for (int i = 0; i < sm.stageArray.GetLength(0); i++) { for (int j = 0; j < sm.stageArray.GetLength(1); j++) { if (sm.stageArray[i, j] == 0) { var obj =Instantiate(floorBlock, new Vector3(i, -1, j), Quaternion.identity) as GameObject; obj.transform.parent = ParentObj.transform; } else { var obj = Instantiate(wallBlock, new Vector3(i, 0, j), Quaternion.identity) as GameObject; obj.transform.parent = ParentObj.transform; } } } }
ステージ遷移のタイミング
前章と似た処理になりますが,Playerのpositionと階段のpositionが一致したときにボタンUIを表示しています. ボタンをおすことによってStateManagerの状態を変更することによってレイヤーを切り替えます
ボタンの表示
if(sm.optionArray[playerPositionX,playerPositionY] == 1){ Debug.Log("階段の上に乗った"); stairsUI.SetActive(true); } else{ stairsUI.SetActive(false); sm.nowState = StateManager.GameState.EnemyTurn; }
ボタンによるステートチェンジ
public void nextStage(){ stairsUI.SetActive(false); Debug.Log("次の階へ"); sm.nowState = StateManager.GameState.CreateStageTurn; } public void stayStage(){ stairsUI.SetActive(false); sm.nowState = StateManager.GameState.EnemyTurn; }
階数によって配列を変更しステージを生成する
switch (nowState) { case GameState.CreateStageTurn: // 階数を1つ増やす rank++; switch(rank){ case 1: stageArray = new int[10, 10]{ {1,1,1,1,1,1,1,1,1,1}, {1,0,0,0,0,0,0,0,0,1}, {1,0,0,0,0,0,0,0,0,1}, {1,1,1,1,1,0,0,0,0,1}, {1,0,0,0,0,0,0,0,0,1}, {1,0,0,0,0,0,0,0,0,1}, {1,0,0,0,0,0,1,1,1,1}, {1,0,0,0,0,0,0,0,0,1}, {1,0,0,0,0,0,0,0,0,1}, {1,1,1,1,1,1,1,1,1,1} }; break; //2階 case 2: //割愛 break; } Debug.Log("ステージ作成"); mons.DeleteMonster(); mons.CreateMonster(); op.DeleteOption(); op.CreateOption(); stage.DeleteStage(); stage.CreateStage(); } }
csvファイルでMAPのデータを管理できるように後々修正入れます.
Unity:敵の移動(偽A*,A-star),攻撃(ローグライクを自分なりの解釈で作ってみる.)
概要
- ローグライクシリーズその4弾
- 敵を移動させたい
- 近くにプレイヤーがいたら攻撃したい
完成図
A*(A-star)って何?
A*とは探索アルゴリズムのことを指します.
いい加減に解釈するとA地点からB地点までの最短距離を導き出してくれます.
偽A-Star実装
今回私はゴールまでの道順ではなく,Enemyの周りのブロックの内,どの場所がプレイヤーに一番近いかを探索します.
まず,Enemy,stage,playerのposition情報を記録している多次元配列を確認します. 配列ごとにレイヤーが分けられていますが今回はわかりやすいようにこの3つのレイヤーを一つに重ねます. すると以下のような位置関係になります
0:床 1:壁 2:Player 3:Monster
1 | 2 | 3 | 4 | 5 | 6 |
---|---|---|---|---|---|
1 | 1 | 1 | 1 | 1 | 1 |
1 | 3 | 0 | 0 | 2 | 1 |
1 | 0 | 0 | 0 | 0 | 1 |
1 | 0 | 0 | 1 | 0 | 1 |
1 | 0 | 0 | 0 | 0 | 1 |
1 | 1 | 1 | 1 | 1 | 1 |
- monster(3)の位置の周りに壁がないかをstage配列から検索します.
この場合monster(3)から見て,上と左が壁になっているので2点には移動できないものと考えて良いでしょう. monster(3)からみて右と下の座標を取得します.
今回の場合,
右「x:3,y:2」
下「x:2,y:3」plyaer(2)のポジション「x5,y2」とmonsterから見た右と下のpositionの差を求めます
abs(monsterPositionX - PlayerPositionX) + abs(monsterPositionY - PlayerPositionY);<br>
この計算によりPlayerまでの移動にかかるコストを求めることが出来ます
今回の例だと
monsterからみた右のマス abs(3-5) + abs(2-2) = コスト2 monsterからみた下のマス abs(2-5) + abs(3-2) = コスト4
これによりmonsterは右のマスに移動することでPlyaerに近づくことができる.
実際に移動してみる
移動の際は物質的な移動と配列の更新が必要です
//物理的な移動 Hashtable moveHash = new Hashtable(); moveHash.Add("position", new Vector3(enemyPositionX + moveX, -0.5f, enemyPositionZ + moveZ)); moveHash.Add("time", 0.4f); moveHash.Add("delay", 0.0f); iTween.MoveTo(monsterList[i], moveHash); //配列の更新 sm.monsterArray[enemyPositionX, enemyPositionZ] = 0; sm.monsterArray[enemyPositionX + moveX, enemyPositionZ + moveZ] = i + 1;
本当は1マス分だけではなくPlayerに近づくための最短ルートをすべてきちんと出すべきだが,今回は簡易版でこの場をしのぎます
ある程度ゲームが完成した後に更に深掘り下げて実装してきます
きちんと作りたい方は下記のリンクがとても参考になると思います
というか参考になりましたありがとうございます.
qiita.com
モンスターの攻撃
モンスターの攻撃を実装してみます.
複雑に考えずにmonsterの周りのマスにプレイヤーがいれば攻撃,いなければ移動します.
攻撃の際にモンスターを回転させる+攻撃アニメーションを再生する.プレイヤーのステータスを変更する事ができればとりあえず良さそうです
//playerがenemyから見た下方向に位置していたら if (sm.playerArray[enemyPositionX, enemyPositionZ -1] == 2) { // monsterを回転 iTween.RotateTo(monsterList[i], iTween.Hash("y", 180, "islocal", true, "time", 0.2f)); // monsterのAttackアニメーションを再生 monsterList[i].GetComponent<Animator>().SetTrigger("Attack"); // playerStatusにダメージを渡す sm.player.GetComponent<PlayerStatus>().Receive(1); // 0.4秒待つ yield return new WaitForSeconds(0.4f); }
Unity:ステート管理(ローグライクを自分なりの解釈で作ってみる)
概要
- ゲームの進行を制御したい
- 特にすごいことをしているわけではないのでマジで自分用のメモ
イメージ
イメージではPlayer→Option→Enemyの順番で行動したい.
なのでPlayerが移動するまではOptionとEnemyは待機,
Option中はEnemyとプレイヤーは待機. そんなイメージ
唯一無二の存在Singleton
ステートの管理を行う場所は一箇所で良い.逆に一箇所でなければなにかと面倒くさい.
そこでSingletonと言うものを利用する.
仕組みはシンプルで,コンストラクタをprivateにすることによって外部からインスタスを作れないようにしてあげます.
そして自分自身がインスタンスを生成してstaticとして外部に公開します.これでそのプログラム内で唯一無二のインスタンスを共有することが出来ます.
詳細を知りたい方は下記のリンクへ 5. Singleton パターン | TECHSCORE(テックスコア)
Unityでどう使うの?ってかコードがほしいって人は下記のリンクがおすすめです naichilab.blogspot.jp
ステート管理
管理には唯一無二の存在が適しています.なので今回作成するプログラムはSingletonにします. コレで外部からの操作も可能となります.
ENUM
まず,大体必要になりそうなステートをEnumに記述していきます
public enum GameState { None = 0, PlayerTurn, OptionTurn, EnemyTurn, }
switchで遷移
nowStateに現在の状態が入力されています
public GameState nowState = GameState.PlayerTurn;
このnowStateを外部のスクリプトから変更することによって下記のswitch文で処理を遷移させます. 今回はわかりやすいようにOptionTurnステートに移動次第二秒間待機しplayerTurnへ遷移させています.
void Update () { //ステートの実行 switch (nowState) { case GameState.PlayerTurn: Debug.Log("player"); // プレイヤーが動く break; case GameState.OptionTurn: nowState = GameState.None; StartCoroutine("StartOption"); break; case GameState.EnemyTurn: //エネミーが移動する break; } } IEnumerator StartOption(){ yield return new WaitForSeconds(2.0f); nowState = GameState.PlayerTurn; }
このステート管理プログラムをベースに開発を進めていきます
Unity:ステージ作成と移動(ローグライクを自分なりの解釈で作ってみる)
完成画像
概要
- ローグライクで使えそうなステージを作り,ステージ内をPlayerで移動する.
- 移動は1マスずつ.
- マップは配列で作成する.
- 壁には移動できないようにすること
仕様
今回私が作るローグライクゲームでは複数のレイヤーを重ねてステージを作ります.
こうすることによってステージの床の上にトラップが仕掛けられていて,その上にアイテムが落ちていることを階層的に再現することが出来ます.
ステージレイヤーとプレイヤーレイヤーの生成
ステージレイヤーとプレイヤーレイヤーの2つの配列を作ってみます
ステージ配列
0:床 1:壁
// ステージ多次元配列 public int[,] stageArray = new int[10, 10]{ {1,1,1,1,1,1,1,1,1,1}, {1,0,0,0,0,0,0,0,0,1}, {1,0,0,0,0,0,0,0,0,1}, {1,0,0,0,0,0,0,0,0,1}, {1,0,0,0,0,0,0,0,0,1}, {1,0,0,0,0,0,0,0,0,1}, {1,0,0,0,0,0,0,0,0,1}, {1,0,0,0,0,0,0,0,0,1}, {1,0,0,0,0,0,0,0,0,1}, {1,1,1,1,1,1,1,1,1,1} };
プレイヤー配列
0:特になし 2:プレイヤー
public int[,] playerArray = new int[10, 10]{ {0,0,0,0,0,0,0,0,0,0}, {0,0,0,0,0,0,0,0,0,0}, {0,0,0,0,0,0,0,0,0,0}, {0,0,0,0,0,0,0,0,0,0}, {0,0,0,0,0,2,0,0,0,0}, {0,0,0,0,0,0,0,0,0,0}, {0,0,0,0,0,0,0,0,0,0}, {0,0,0,0,0,0,0,0,0,0}, {0,0,0,0,0,0,0,0,0,0}, {0,0,0,0,0,0,0,0,0,0} };
この情報をもとにマップを生成してみます.
マップ生成コード
for (int i = 0; i < stageArray.GetLength(0); i++) { for (int j = 0; j < stageArray.GetLength(1); j++) { if (stageArray[i,j] == 0) { Instantiate(floorBlock, new Vector3(i, -1, j), Quaternion.identity); } else { Instantiate(wallBlock, new Vector3(i, 0, j), Quaternion.identity); } } }
それぞれの要素の場所自体がUnity内の座標とを同期させてあげています.
なのでStage配列の1-6番目の要素はX:6,Y:6のpositionということになります.
あとはその場所にCubeを生成してあげれば完了です
私がここで詰まったところ。
ものすごくお恥ずかしいのですが多次元配列の,要素数を取得する方法を知りませんでした. 純粋な配列の場合は
array.Length;
で問題ないのですが,多次元配列の場合は下記のように取得することが出来ます
array.GetLength(hoge);
こういう小さいことからちゃんとツメていきたいですね....
キャラクタの移動
ユーザーの入力に反応
今回はASDWKEYに対応して移動させます.
void Update(){ int moveX; int moveZ; // Keyによってプレイヤーが相対的に何処に移動するかを指定する if (Input.GetKeyDown(KeyCode.A)) { moveX = -1; moveZ = 0; UpdatePlayerPosition(moveX, moveZ); } else if (Input.GetKeyDown(KeyCode.D)) { moveX = 1; moveZ = 0; UpdatePlayerPosition(moveX, moveZ); } else if (Input.GetKeyDown(KeyCode.W)) { moveX = 0; moveZ = 1; UpdatePlayerPosition(moveX, moveZ); } else if (Input.GetKeyDown(KeyCode.S)) { moveX = 0; moveZ = -1; UpdatePlayerPosition(moveX, moveZ); } else{ moveX = 0; moveZ = 0; UpdatePlayerPosition(moveX, moveZ); } }
KeyDownを検知次第,Playerを相対的に何処に移動するかを決めます.
その後UpdatePlayerPositionに値を渡して移動を開始します
UpdatePlayerPosition
プレイヤーのポジション取得
プレイヤーがどこにいるかを配列から探してきます
int playerPositionX = 0; int playerPositionZ = 0; //プレイヤーの現在の一を取得する for (int i = 0; i < playerArray.GetLength(0); i++) { for (int j = 0; j < playerArray.GetLength(1); j++) { if (playerArray[i, j] == 2) { playerPositionX = i; playerPositionZ = j; } } }
壁判定と配列更新と移動
一気に3つ書くのはどうかと思いますがこれらは絡みまくってるので一気にまとめてかいちゃいます
// 移動先が壁だった場合はreturnそれ以外の場合はプレイヤーをitweenによって移動させ,配列を更新する. if (stageArray[playerPositionX + moveX, playerPositionZ + moveZ] == 1) { Debug.Log("壁"); return; } else { playerArray[playerPositionX, playerPositionZ] = 0; playerArray[playerPositionX + moveX, playerPositionZ + moveZ] = 2; Hashtable moveHash = new Hashtable(); Debug.Log(playerPositionX); moveHash.Add("position", new Vector3(playerPositionX, transform.position.y, playerPositionZ)); moveHash.Add("time", 0.4f); moveHash.Add("delay", 0.0f); iTween.MoveTo(player, moveHash); }
壁判定
if (stageArray[playerPositionX + moveX, playerPositionZ + moveZ] == 1) { Debug.Log("壁"); return; } else {
現在のプレイヤーPositionの周りに壁がないかStage配列から調べます.
プレイヤーの配列の更新
移動先の配列要素を2(プレイヤー)に変更して,移動前の配列要素を0にします
playerArray[playerPositionX, playerPositionZ] = 0; playerArray[playerPositionX + moveX, playerPositionZ + moveZ] = 2;
プレイヤーの移動
プレイヤーの移動にはiTweenを利用します
Hashtable moveHash = new Hashtable(); moveHash.Add("position", new Vector3(playerPositionX + moveX, transform.position.y, playerPositionZ+ moveZ)); moveHash.Add("time", 0.4f); moveHash.Add("delay", 0.0f); iTween.MoveTo(player, moveHash);
ざっと説明すると
moveHash.Add("position"......では実際に移動する先のpositionを入力します
moveHash.Add("time".....では移動にかかる時間を指定
moveHash.Add("delay"......では移動開始までにかかる時間を指定
最後のiTween.MoveTo(player,moveHash)により,プレイヤーを動作させます.
Unity:ローグライクを自分なりの解釈で作ってみる.その1
概要
ローグライクって何?
私の説明より下記の記事の説明のほうがわかりやすいです gamy.jp
マップの考え方.
マップのレイヤー構造化
MAPは複数個のレイヤーを利用して生成します.
ステージレイヤー
トラップレイヤー
アイテムレイヤー
これによりデータの量は増えますが管理しやすくなると思います. 例えば,ステージレイヤーにアイテム情報やトラップ情報も入れてしまうとマップのタイルの上にトラップが仕掛けてあってその上にアイテムが置いてある事,の再現が鬼難しくなります.レイヤーを分けて重ねることによってこれを解決できます.
レイヤーの中身(多次元配列マップ)
各々のレイヤーは自動生成しやすい形で作ってあげたいです. そこで多次元配列によりマップを作ってみます
数値とその意味
0:床 1:壁
マップの形
1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
---|---|---|---|---|---|---|---|
1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
1 | 0 | 0 | 0 | 0 | 0 | 0 | 1 |
1 | 0 | 0 | 0 | 0 | 0 | 0 | 1 |
1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
マップの配列
[ [11111111], [1000001], [1100001], [11111111] ]
ざっとこのような形でしょうか
次にマップに階段,トラップ,アイテムを付け加えます. ここでマップの配列に直接値を埋め込むのではなく,レイヤーを分けてそれらの情報を追加します.
数値とその意味
0:何も置かない 1:階段 2:トラップ
トラップ,階段の配置
1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
---|---|---|---|---|---|---|---|
0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 |
0 | 0 | 2 | 0 | 2 | 0 | 0 | 0 |
0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
キャラクターの移動
過去に移動についてのまとめていたので下記を参照 zvn-x.hatenablog.com
ゲーム進行・ステート管理
マップの生成と移動は完了したので次にゲームの遷移について考えていきます. チョコボの不思議なダンジョンを参考にするとざっとこのような流れでしょうか.
もちろんその他もろもろの処理は必要ですが!はじめは複雑に考えずにシンプルに考えていきたいので現状ではこれでいいはず
CSVファイルでモンスターのステータスの管理
モンスターのステータスは後々楽に編集・管理にCSVファイルを利用します.
ID | NAME | HP | SPEED | ATK | DEF |
---|---|---|---|---|---|
001 | スライム | 32 | 3 | 10 | 3 |
002 | ゴーレム | 100 | 3 | 10 | 12 |
003 | トール | 1200 | 2 | 100 | 10 |
他にも状態異常耐性なども加えていきたいですが後々加えていけばいいと思います.
もちろんアイテムも同じ要領で管理します.
今後
今後以下の手順でゲームを作成していく
- [x] stageの作成
- [x] キャラクタ移動
- [x] ゲームステート
- [x] 階段作成
- [x] モンスターマップ作成
- [x] モンスター移動
- [x] player攻撃
- [ ] プレイヤー・モンスターステータス管理
- [ ] アイテム処理
- [ ] マップ自動生成
- [ ] csvによるデータ管理
- [ ] タイトル画面+ステージセレクト画面+GameOver時の処理追加
ダンジョンを1マスずつ進むローグライク(チョコボの不思議なダンジョン)的なもの
こちらのページが最新版です
概要
- チョコボのように1マスずつの移動を実装する
- マップの壁には移動できないようにする.
- iTweenで滑らかに移動
設計もどき
- ステージを配列で作ってみます.
0:壁 1:床 2:Player 1:{0,0,0,0,0,0,0}, 2:{0,1,1,1,1,1,1,0}, 3:{0,1,1,1,1,1,1,0}, 4:{0,1,1,1,1,1,1,0}, 5:{0,0,0,0,0,1,0},
- キャラクターの位置を管理する配列も作っていきます
0: 1: 2:Player 1:{0,0,0,0,0,0,0}, 2:{0,0,0,0,0,0,0}, 3:{0,0,0,0,0,2,0}, 4:{0,0,0,0,0,0,0}, 5:{0,0,0,0,0,0,0},
この時キャラクターは3-6Positionにいます. ステージ配列の中の3-6の右隣は0,それ以外は1なので プレイヤーは2-6,4-6,3-5に移動できることがわかる
これをプログラム上で再現すればよいのである.
実装
今回はシンプルな配列を用意しその中で,左の移動のみを行う
ステージ配列
[0,1,1,0]
Player
[0,0,2,0]
コード
void Update () { if(Input.GetKey(KeyCode.A)){ // Player配列からPlayerの位置を検索 int index1 = player.IndexOf(2); // 配列の範囲内ででなければreturn if (index1 == 0) { Debug.Log("限界値"); return; } // プレイヤーから見て左の場所が壁かを判定 if(stage[index1-1] != 0){ Debug.Log("壁じゃなかった場合"); transform.position = new Vector3(index1 - 1, transform.position.y, transform.position.z); // 配列上のプレイヤー位置を更新する player[index1] = 0; player[index1 - 1] = 2; } else{ Debug.Log("左は壁だよ"); } } }
iTweenで動きをなめらかに
上記のコードではボタンを押した瞬間に一瞬で移動しちゃいますよね
なのでiTweenを利用して楽させていただきましょう
なめらかな移動
Hashtable moveHash = new Hashtable(); moveHash.Add("position", new Vector3(index1 - 1, transform.position.y, transform.position.z)); moveHash.Add("time", 0.2f); moveHash.Add("delay", 0.0f); iTween.MoveTo(gameObject, moveHash);
応用
今回はStageとPlayerしか作っていませんが,モンスター配列やアイテム配列,その他の配列があると更にゲームらしくなりそうですね. ステージを配列で管理していると敵の自動生成,アイテムの自動生成も容易にできそうですよね.
ラムダ式を超簡単に解釈してコールバック処理を書いてみる
概要
ラムダ式の根本を全て理解しようとすると意外と面倒くさいので上辺だけの理解でコールバック処理を作ってみる
ラムダ式とは?
匿名メソッドや匿名関数を簡易的な書式で表現できちゃう。その書式がラムダ式
超ガバガバ解釈にすると 「メソッドを変数に入れられちゃうよ」ってわけ
一番簡単なラムダ式
Action
Action<int> SquaringValue = (x) =>{ x = x * x; Debug.Log(x); }; SquaringValue(2);
ざざっと説明すると,
intは見ればわかるように引数の型を指定します,実際に引数の変数は(x)です.
矢印=>後に実際に行いたい処理を書いていきます.ステートメントブロックやね
以上です.複雑に考えなければ大したことはありませんね。(本当はちゃんと理解して使うべき)
コールバック処理を作ってみる
そもそもコールバックとは?
私は処理の委託だと考えています.
ラムダ式で考えるなら,メソッドへの参照を一つのオブジェクトとして扱い.メソッド参照を持つオブジェクトを転送します.
転送先でメソッド参照を持つオブジェクトを呼び出すことがコールバック
コード
とっても簡単です!メソッドを変数に入れて別メソッドに渡すだけです
メソッドの参照を持つオブジェクトを作り,オブジェクトをhoge関数に渡します
void Start () { Action<int> SquaringValue = (x) => { x = x * x; Debug.Log(x); }; hoge(SquaringValue,2); }
受け取ったオブジェクトに数値を渡してこちらでメソッドを呼び出します
void hoge(Action<int> ac, int value) { value = value * 2; ac(value); }