パックマンライクなゲーム制作の振り返りになります。※連番はやめました。
ゲームは↓で遊べるので、よければ遊んでみてください。
前の投稿はこちらです。
今回のテーマ
今回のテーマは、NPCキャラクターの行動パターンです。パックマンライクですので、敵キャラクターに追いかけられて、触れられたらゲームオーバーという挙動を作っていく必要があります。
最終的にはモンスターごとに独自の動きをつけることもしましたが、今回は基本的な動作の部分をまずは振り返ってみたいと思います。なお、今回の動作は基本的に自分で考えたプログラムになります。AIを使って学習させる、というライブラリもあるようですが、ひとまず、自分で考えてみるということが一個のテーマでもあったので、最適な方法は考える余地があるかもしれませんが、いったん、その時やってきたことをふりかえるということで書いていきたいと思います。
なお、unityの2Dゲームでキャラクターを移動させる処理については、以下の記事も参考にしてみてください。
考え方
まず、基本的な考え方ですが、フィールドを一つの大きさをtransformの1×1の大きさのマス目に区切った状態を考えます(図参照、以下一つ一つのブロックはマスと書きます)。この時、進める方向は上下左右の4方向が候補になります。プログラムで斜め移動も可能ではあるのですが、今回はゲームの性質を考えて、4方向に絞りました。移動のプログラムをこの後考えていきますが、基本的には上下左右のどちらかに進む→状況を判断する→次に進む方向を決める→また上下左右のどちらかに進む…ということを繰り返すことになります。
肝になるのは状況判断から次の方向を決めるところで、自然な動きとゲーム性のバランスを取りながら、どのように動かすか、というところを四苦八苦しながら作っていきました。
動作の設計
動作を決める状況判断の要素は、大きく以下の2つになります。
- 進める方向
- プレイヤーキャラクターとの位置関係
そして、状況と進行方向の判断は以下のようなルールでおおよそ決まっていきます。※プログラムと同じで上から結果を参照していくものと思ってください
- 敵キャラクターからプレイヤーが見えているか?
- Yes→進行方向はプレイヤーのいる方向
- 進める方向が1方向だけ
- Yes→進める方向に進む
- 進める方向が2方向ある
- Yes→それまで進んでいた方向に進み続ける、または曲がる
- 進める方向が3方向以上ある
- Yes→プレイヤーキャラクターのいる方向に向かう、ランダムで別の方向に向かう
一つ一つ解説していきます。
プレイヤーキャラクターが見えているか
これは敵キャラクターから、プレイヤーキャラクターの姿が見えている時は、プレイヤーキャラクターを追いかけるように動くというルールです。最初、この処理を入れずに動作させていたのですが、プレイヤーキャラクターが近くにいる時も、敵キャラクターが曲がって遠回りしてしまうため、動作としても不自然ですし、ゲームバランス的にも(ルールを理解していれば)簡単に敵を避けることが可能になってしまったため、プレイヤーが見えている場所ではプレイヤーを追いかけるように設定しました。
設定の仕方はRaycastを使って、敵プレイヤーの視線を表現しています。Raycastの詳細は余裕があれば、別途書いてみたいと思いますが、ここでは触れません。簡単に言うと、上のイメージのようにゲーム内に見えない光線を飛ばして光線のあたったオブジェクトの情報を取得できる機能になります。今回のように視線のような使い方もできれば、シューティングゲームの弾道のような使い方もあるようです。
本ゲーム内では、敵の向きを考慮して、前と横の情報を取得してくるようにRaycastを飛ばします。これはPlayer_watchという関数にして、以下のようにプログラムしました。
public virtual bool Player_watch()
{
if (move_direction == "up")
{
Ray2D watch_ray = new Ray2D(new Vector3(this_x, this_y + 0.51f, 0), new Vector3(0, 1, 0));
RaycastHit2D watch_hit = Physics2D.Raycast(watch_ray.origin, watch_ray.direction);
if(watch_hit.collider.gameObject.name == "Player")
{
UpDirection();
return true;
}
watch_ray = new Ray2D(new Vector3(this_x + 0.51f, this_y, 0), new Vector3(1, 0, 0));
watch_hit = Physics2D.Raycast(watch_ray.origin, watch_ray.direction);
if (watch_hit.collider.gameObject.name == "Player")
{
RightDirection();
return true;
}
watch_ray = new Ray2D(new Vector3(this_x - 0.51f, this_y, 0), new Vector3(-1, 0, 0));
watch_hit = Physics2D.Raycast(watch_ray.origin, watch_ray.direction);
if (watch_hit.collider.gameObject.name == "Player")
{
LeftDirection();
return true;
}
return false;
}
else if (move_direction == "down")
{
Ray2D watch_ray = new Ray2D(new Vector3(this_x, this_y - 0.51f, 0), new Vector3(0, -1, 0));
RaycastHit2D watch_hit = Physics2D.Raycast(watch_ray.origin, watch_ray.direction);
if (watch_hit.collider.gameObject.name == "Player")
{
DownDirection();
return true;
}
watch_ray = new Ray2D(new Vector3(this_x + 0.51f, this_y, 0), new Vector3(1, 0, 0));
watch_hit = Physics2D.Raycast(watch_ray.origin, watch_ray.direction);
if (watch_hit.collider.gameObject.name == "Player")
{
RightDirection();
return true;
}
watch_ray = new Ray2D(new Vector3(this_x - 0.51f, this_y, 0), new Vector3(-1, 0, 0));
watch_hit = Physics2D.Raycast(watch_ray.origin, watch_ray.direction);
if (watch_hit.collider.gameObject.name == "Player")
{
LeftDirection();
return true;
}
return false;
}
else if (move_direction == "right")
{
Ray2D watch_ray = new Ray2D(new Vector3(this_x, this_y + 0.51f, 0), new Vector3(0, 1, 0));
RaycastHit2D watch_hit = Physics2D.Raycast(watch_ray.origin, watch_ray.direction);
if (watch_hit.collider.gameObject.name == "Player")
{
UpDirection();
return true;
}
watch_ray = new Ray2D(new Vector3(this_x + 0.51f, this_y, 0), new Vector3(1, 0, 0));
watch_hit = Physics2D.Raycast(watch_ray.origin, watch_ray.direction);
if (watch_hit.collider.gameObject.name == "Player")
{
RightDirection();
return true;
}
watch_ray = new Ray2D(new Vector3(this_x, this_y - 0.51f, 0), new Vector3(0, -1, 0));
watch_hit = Physics2D.Raycast(watch_ray.origin, watch_ray.direction);
if (watch_hit.collider.gameObject.name == "Player")
{
DownDirection();
return true;
}
return false;
}
else if (move_direction == "left")
{
Ray2D watch_ray = new Ray2D(new Vector3(this_x, this_y + 0.51f, 0), new Vector3(0, 1, 0));
RaycastHit2D watch_hit = Physics2D.Raycast(watch_ray.origin, watch_ray.direction);
if (watch_hit.collider.gameObject.name == "Player")
{
UpDirection();
return true;
}
watch_ray = new Ray2D(new Vector3(this_x, this_y - 0.51f, 0), new Vector3(0, -1, 0));
watch_hit = Physics2D.Raycast(watch_ray.origin, watch_ray.direction);
if (watch_hit.collider.gameObject.name == "Player")
{
DownDirection();
return true;
}
watch_ray = new Ray2D(new Vector3(this_x - 0.51f, this_y, 0), new Vector3(-1, 0, 0));
watch_hit = Physics2D.Raycast(watch_ray.origin, watch_ray.direction);
if (watch_hit.collider.gameObject.name == "Player")
{
LeftDirection();
return true;
}
return false;
}
return false;
}
最終的なプログラムは最後に書くので、それも参照してもらいたいですが、プログラムの中身について、少し補足しておきます。
まず、move_directionというのは、その敵キャラクターの向いている方角を表す変数になります。例えばupであれば、上を向いているという感じです。
移動中は向いている方向に進み、Player_watch内では、その向きに対して前と左右にプレイヤーキャラクターがいるかどうかをチェックしています(後ろはチェックしていません)。〇〇Direction()と書いてある関数は次に向かう方向を決める関数です(これも詳細は最後のソースコードを見てください)。関数の中で次に向かうマスとmove_direction、キャラクターのアニメーションをセットしています。
進める方向が1方向ならそちらに進む
続いて、進める方向をチェックしています。進める方向の数は1,2,3,4のどれかになりますので、それぞれで場合分けして、処理を書いていきます。なお、3と4は結果的に同じになるので、一つの処理にまとめています。
まず、1方向の処理ですが動きは1方向しかないので、進める方向に進むことになります。プログラムは以下のようになります。なお、1方向になる場合としては、袋小路に到達したか、道はあっても敵キャラクターが周囲にいた場合に起こります。
// 進める方向の確認
var moving_available = new Dictionary<string, bool>();
moving_available = MoveAvailabeleDistance();
(略)
// 3方向埋まっているなら、1方向に決定
else if (moving_available["up"] && moving_available["right"] && moving_available["down"])
{
LeftDirection();
}
else if (moving_available["up"] && moving_available["left"] && moving_available["down"])
{
RightDirection();
}
else if (moving_available["left"] && moving_available["right"] && moving_available["down"])
{
UpDirection();
}
else if (moving_available["up"] && moving_available["right"] && moving_available["left"])
{
DownDirection();
}
moving_availableは辞書型で進めない方向がbool値でセットしてあります(言葉の意味的に逆になってしまっていますが、ただのミスなので、進めない方向だと思ってください)。見た通り、シンプルに進める方向の〇〇Direction関数を実行しているだけです。
moving_availableはMoveAvailabeleDistance()という関数を用意して、状況確認のたびに取得してきています。
Dictionary<string, bool> MoveAvailabeleDistance()
{
Ray2D up_ray = new Ray2D(new Vector3(this_x, this_y + 0.51f, 0), new Vector3(0, 1, 0));
RaycastHit2D up_hit = Physics2D.Raycast(up_ray.origin, up_ray.direction, 0.6f);
Ray2D down_ray = new Ray2D(new Vector3(this_x, this_y - 0.51f, 0), new Vector3(0, -1, 0));
RaycastHit2D down_hit = Physics2D.Raycast(down_ray.origin, down_ray.direction, 0.6f);
Ray2D right_ray = new Ray2D(new Vector3(this_x + 0.51f, this_y, 0), new Vector3(1, 0, 0));
RaycastHit2D right_hit = Physics2D.Raycast(right_ray.origin, right_ray.direction, 0.6f);
Ray2D left_ray = new Ray2D(new Vector3(this_x - 0.51f, this_y, 0), new Vector3(-1, 0, 0));
RaycastHit2D left_hit = Physics2D.Raycast(left_ray.origin, left_ray.direction, 0.6f);
//Debug.DrawRay(up_ray.origin, up_ray.direction * 0.2f, Color.red, 1f, false);
//Debug.DrawRay(down_ray.origin, down_ray.direction * 0.2f, Color.red, 1f, false);
//Debug.DrawRay(right_ray.origin, right_ray.direction * 0.2f, Color.red, 1f, false);
//Debug.DrawRay(left_ray.origin, left_ray.direction * 0.2f, Color.red, 1f, false);
var moving_available = new Dictionary<string, bool>();
if (up_hit)
{
if (LayerMask.LayerToName(up_hit.collider.gameObject.layer) == "Enemy" || LayerMask.LayerToName(up_hit.collider.gameObject.layer) == "Obstract")
{
bool up_hit_bool = true;
moving_available.Add("up", up_hit_bool);
}
else
{
bool up_hit_bool = false;
moving_available.Add("up", up_hit_bool);
}
}
else
{
bool up_hit_bool = false;
moving_available.Add("up", up_hit_bool);
}
if (down_hit)
{
if (LayerMask.LayerToName(down_hit.collider.gameObject.layer) == "Enemy" || LayerMask.LayerToName(down_hit.collider.gameObject.layer) == "Obstract")
{
bool down_hit_bool = true;
moving_available.Add("down", down_hit_bool);
}
else
{
bool down_hit_bool = false;
moving_available.Add("down", down_hit_bool);
}
}
else
{
bool down_hit_bool = false;
moving_available.Add("down", down_hit_bool);
}
if (right_hit)
{
if (LayerMask.LayerToName(right_hit.collider.gameObject.layer) == "Enemy" || LayerMask.LayerToName(right_hit.collider.gameObject.layer) == "Obstract")
{
bool right_hit_bool = true;
moving_available.Add("right", right_hit_bool);
}
else
{
bool right_hit_bool = false;
moving_available.Add("right", right_hit_bool);
}
}
else
{
bool right_hit_bool = false;
moving_available.Add("right", right_hit_bool);
}
if (left_hit)
{
if (LayerMask.LayerToName(left_hit.collider.gameObject.layer) == "Enemy" || LayerMask.LayerToName(left_hit.collider.gameObject.layer) == "Obstract")
{
bool left_hit_bool = true;
moving_available.Add("left", left_hit_bool);
}
else
{
bool left_hit_bool = false;
moving_available.Add("left", left_hit_bool);
}
}
else
{
bool left_hit_bool = false;
moving_available.Add("left", left_hit_bool);
}
return moving_available;
}
Player_watchと同様にRaycastを使用して、周囲の状況を取得しています。Player_watchと異なるのは、Raycastの長さを指定していることです。Player_watchは視線ですので、長さに距離を設定していませんが、MoveAvailabeleDistanceでは周囲約1マス分だけ情報を取得してくるようにしています。
この時、プレイヤーキャラクターにrayがヒットした場合は無視するため、ヒットしたオブジェクトのレイヤーを取得して、敵キャラクター同士か障害物(木のオブジェクト)だった場合のみtrueにしています。
進める方向が2方向の場合
進める方向が2方向の場合の基本的な考え方は、進んでいる方向にそのまま進み、戻らないということです。戻るようなプログラムになっていると、行ったり来たりを繰り返す不自然な動作を行ってしまう可能性があります。
プログラムは以下になります。進んでいる方向に対して、曲がり角になっていれば曲がる。まっすぐであればそのまま前に進むようになっています。
// 2方向しかないなら移動している方向に移動
else if (moving_available["up"] && moving_available["right"])
{
if (move_direction == "right" || move_direction == "init")
{
DownDirection();
}
else
{
LeftDirection();
}
}
else if (moving_available["up"] && moving_available["left"])
{
if (move_direction == "left" || move_direction == "init")
{
DownDirection();
}
else
{
RightDirection();
}
}
else if (moving_available["down"] && moving_available["right"])
{
if (move_direction == "right" || move_direction == "init")
{
UpDirection();
}
else
{
LeftDirection();
}
}
else if (moving_available["down"] && moving_available["left"])
{
if (move_direction == "left" || move_direction == "init")
{
UpDirection();
}
else
{
RightDirection();
}
}
else if (moving_available["up"] && moving_available["down"])
{
if (move_direction == "left" || move_direction == "init")
{
LeftDirection();
}
else
{
RightDirection();
}
}
else if (moving_available["left"] && moving_available["right"])
{
if (move_direction == "up" || move_direction == "init")
{
UpDirection();
}
else
{
DownDirection();
}
}
特別解説するところはないですが、move_direction == “init”というのは、向きのまだ決まっていない敵キャラクターの登場時に設定しているパラメータです。
進める方向が3方向以上の場合
進める方向が3つ以上ある場合は、プレイヤーキャラクターのいる大まかな方向に対して進みます。これは、例えば物音や気配を感じて、なんとなく怪しいなと思う方向に進むイメージですね。ゲームの難易度的にもあんまり適当な方向に進ませると避けるのが簡単になってしまうので、基本的にはプレイヤーの方に向かうようにしています。
// 3つ以上の選択肢があるなら、主人公のいる方向に進む
else
{
distance_result = PlayerDistanceCheck();
if (distance_result[0] == true && distance_result[1] == false) // 左上
{
if (hostility >= hostility_threshold)
{
if(moving_available["down"] == false)
{
DownDirection();
}
else
{
RightDirection();
}
}
else if((move_direction == "up" || move_direction == "down") && moving_available["left"] == false)
{
LeftDirection();
}
else if (moving_available["up"] == false)
{
UpDirection();
}
else
{
LeftDirection();
}
}
else if (distance_result[0] == false && distance_result[1] == false) // 右上
{
if (hostility >= hostility_threshold)
{
if (moving_available["down"] == false)
{
DownDirection();
}
else
{
LeftDirection();
}
}
else if ((move_direction == "up" || move_direction == "down") && moving_available["right"] == false)
{
RightDirection();
}
else if (moving_available["up"] == false)
{
UpDirection();
}
else
{
RightDirection();
}
}
else if (distance_result[0] == true && distance_result[1] == true) // 左下
{
if (hostility >= hostility_threshold)
{
if (moving_available["up"] == false)
{
UpDirection();
}
else
{
RightDirection();
}
}
else if ((move_direction == "up" || move_direction == "down") && (moving_available["left"] == false || move_direction == "left"))
{
LeftDirection();
}
else if (moving_available["down"] == false)
{
DownDirection();
}
else
{
LeftDirection();
}
}
else if (distance_result[0] == false && distance_result[1] == true) // 右下
{
if (hostility >= hostility_threshold)
{
if (moving_available["up"] == false)
{
UpDirection();
}
else
{
LeftDirection();
}
}
else if ((move_direction == "up" || move_direction == "down") && moving_available["right"] == false)
{
RightDirection();
}
else if (moving_available["down"] == false)
{
DownDirection();
}
else
{
RightDirection();
}
}
}
プレイヤーのいる方向はPlayerDistanceCheck()という関数でチェックしています。ちょっとわかりにくいですが、リストになっていて、敵の位置に対して[0]が左側にいるか、[1]が下側にいるか、という意味になります。bool値が2種類あるので、左上、右上、左下、右下、をそれぞれ表せるようになっています。hostilityについては後述するとして、それ以外の部分は、基本的に曲がれるときは曲がるような設定をしています。
hostilityについて
hostility_thresholdは0~1の間の確率を設定する変数になっています。Update毎に0~1のランダムな値を取得するhostilityと比較して、hostility<hostility_thresholdになっている時、プレイヤーのいる方向に進み、逆にhostility>hostility_thresholdになっている時は遠ざかるように動きます。
hostility_thresholdを1の状態(つまり、ランダム性をなくし、必ず特定の状態で決まった動きしかしない状態)にしてしまうと、動きが単調になってしまい、ゲームとしてつまらなくなってしまいます。また、動きが完全に固定化されてしまうので、プログラムの中身を理解していると、敵キャラクターをハメるようなことができてしまい、ゲームバランス的にも面白くありません。
そういった状況を避けるため、一定の確率でランダムな動きをするよう、hostilityを設定しました。hostility_thresholdの値はテストプレイを繰り返して、それなりの動きになるよう調整しています。
その他
ここまでのコードでおおよそ、敵キャラクターの行動パターンは完成です。ただし、実際のゲームではこのままだと問題が起こります。敵キャラクターが1体だけなら問題ないのですが、2体以上出てくる場合(実際、出てくるのですが)、敵キャラクター同士が衝突してしまう可能性があります。
このコードでは敵キャラクターに衝突しても目的のマスに到達するまで進み続けようとするので、ぶつかったまま、押し合うような形でその場に留まってしまいます。それを避けるために、OnCollisionEnter2Dを使って、敵同士がぶつかった場合、反対方向に戻る、という処理を入れています。また、何らかの状況で木のオブジェクトにめり込んでしまう場合があったので、その場合も目的地を戻して、動作を続けるような処理を入れています。
OnCollisionEnter2Dなどの説明は、これも別途まとめられればと思いますが、オブジェクトの衝突判定を行う時の関数だと思ってください。
最終的なソースコード
これまでのルールをまとめると以下のようなスクリプトが出来上がります。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;
public class EnemyController : MonoBehaviour
{
[SerializeField] private Animator EnemyrAnim;
[SerializeField] public float moveSpeed;
public Rigidbody2D rb;
public GameObject Player;
public Vector3 player_vector;
public float player_x;
public float player_y;
public Vector3 this_vector;
public float this_x;
public float this_y;
public List<bool> distance_result;
public Vector3 move_target;
public string move_direction;
public float hostility; // プレイヤーに近づく確率
public float hostility_threshold; // プレイヤーに近づく確率の閾値
// Start is called before the first frame update
void Start()
{
hostility_threshold = 0.7f;
InitalizeMoveSpeed();
move_target = this.transform.position;
move_direction = "init";
Player = GameObject.Find("Player");
}
public virtual void InitalizeMoveSpeed()
{
moveSpeed = 2.0f;
}
// Update is called once per frame
public virtual void Update()
{
hostility = Random.Range(0.0f, 1.0f);
// 位置情報の更新
player_vector = Player.transform.position;
player_x = player_vector.x;
player_y = player_vector.y;
this_vector = this.transform.position;
this_x = this_vector.x;
this_y = this_vector.y;
// 移動先にいるなら移動先を更新
if (move_target == this.transform.position)
{
// 進める方向の確認
var moving_available = new Dictionary<string, bool>();
moving_available = MoveAvailabeleDistance();
// プレイヤー視認
bool watch_player;
watch_player = Player_watch();
if(watch_player)
{
}
// 3方向埋まっているなら、1方向に決定
else if (moving_available["up"] && moving_available["right"] && moving_available["down"])
{
LeftDirection();
}
else if (moving_available["up"] && moving_available["left"] && moving_available["down"])
{
RightDirection();
}
else if (moving_available["left"] && moving_available["right"] && moving_available["down"])
{
UpDirection();
}
else if (moving_available["up"] && moving_available["right"] && moving_available["left"])
{
DownDirection();
}
// 2方向しかないなら移動している方向に移動
else if (moving_available["up"] && moving_available["right"])
{
if (move_direction == "right" || move_direction == "init")
{
DownDirection();
}
else
{
LeftDirection();
}
}
else if (moving_available["up"] && moving_available["left"])
{
if (move_direction == "left" || move_direction == "init")
{
DownDirection();
}
else
{
RightDirection();
}
}
else if (moving_available["down"] && moving_available["right"])
{
if (move_direction == "right" || move_direction == "init")
{
UpDirection();
}
else
{
LeftDirection();
}
}
else if (moving_available["down"] && moving_available["left"])
{
if (move_direction == "left" || move_direction == "init")
{
UpDirection();
}
else
{
RightDirection();
}
}
else if (moving_available["up"] && moving_available["down"])
{
if (move_direction == "left" || move_direction == "init")
{
LeftDirection();
}
else
{
RightDirection();
}
}
else if (moving_available["left"] && moving_available["right"])
{
if (move_direction == "up" || move_direction == "init")
{
UpDirection();
}
else
{
DownDirection();
}
}
// 3つ以上の選択肢があるなら、プレイヤーキャラクターのいる方向に進む
else
{
distance_result = PlayerDistanceCheck();
if (distance_result[0] == true && distance_result[1] == false) // 左上
{
if (hostility >= hostility_threshold)
{
if(moving_available["down"] == false)
{
DownDirection();
}
else
{
RightDirection();
}
}
else if((move_direction == "up" || move_direction == "down") && moving_available["left"] == false)
{
LeftDirection();
}
else if (moving_available["up"] == false)
{
UpDirection();
}
else
{
LeftDirection();
}
}
else if (distance_result[0] == false && distance_result[1] == false) // 右上
{
if (hostility >= hostility_threshold)
{
if (moving_available["down"] == false)
{
DownDirection();
}
else
{
LeftDirection();
}
}
else if ((move_direction == "up" || move_direction == "down") && moving_available["right"] == false)
{
RightDirection();
}
else if (moving_available["up"] == false)
{
UpDirection();
}
else
{
RightDirection();
}
}
else if (distance_result[0] == true && distance_result[1] == true) // 左下
{
if (hostility >= hostility_threshold)
{
if (moving_available["up"] == false)
{
UpDirection();
}
else
{
RightDirection();
}
}
else if ((move_direction == "up" || move_direction == "down") && (moving_available["left"] == false || move_direction == "left"))
{
LeftDirection();
}
else if (moving_available["down"] == false)
{
DownDirection();
}
else
{
LeftDirection();
}
}
else if (distance_result[0] == false && distance_result[1] == true) // 右下
{
if (hostility >= hostility_threshold)
{
if (moving_available["up"] == false)
{
UpDirection();
}
else
{
LeftDirection();
}
}
else if ((move_direction == "up" || move_direction == "down") && moving_available["right"] == false)
{
RightDirection();
}
else if (moving_available["down"] == false)
{
DownDirection();
}
else
{
RightDirection();
}
}
}
}
// ターゲットの場所にいないならその方向に進む
else
{
this.transform.position = Vector3.MoveTowards(this.transform.position, move_target, moveSpeed * Time.deltaTime);
}
}
bool player_left;
bool player_down;
public virtual bool Player_watch()
{
if (move_direction == "up")
{
Ray2D watch_ray = new Ray2D(new Vector3(this_x, this_y + 0.51f, 0), new Vector3(0, 1, 0));
RaycastHit2D watch_hit = Physics2D.Raycast(watch_ray.origin, watch_ray.direction);
if(watch_hit.collider.gameObject.name == "Player")
{
UpDirection();
return true;
}
watch_ray = new Ray2D(new Vector3(this_x + 0.51f, this_y, 0), new Vector3(1, 0, 0));
watch_hit = Physics2D.Raycast(watch_ray.origin, watch_ray.direction);
if (watch_hit.collider.gameObject.name == "Player")
{
RightDirection();
return true;
}
watch_ray = new Ray2D(new Vector3(this_x - 0.51f, this_y, 0), new Vector3(-1, 0, 0));
watch_hit = Physics2D.Raycast(watch_ray.origin, watch_ray.direction);
if (watch_hit.collider.gameObject.name == "Player")
{
LeftDirection();
return true;
}
return false;
}
else if (move_direction == "down")
{
Ray2D watch_ray = new Ray2D(new Vector3(this_x, this_y - 0.51f, 0), new Vector3(0, -1, 0));
RaycastHit2D watch_hit = Physics2D.Raycast(watch_ray.origin, watch_ray.direction);
if (watch_hit.collider.gameObject.name == "Player")
{
DownDirection();
return true;
}
watch_ray = new Ray2D(new Vector3(this_x + 0.51f, this_y, 0), new Vector3(1, 0, 0));
watch_hit = Physics2D.Raycast(watch_ray.origin, watch_ray.direction);
if (watch_hit.collider.gameObject.name == "Player")
{
RightDirection();
return true;
}
watch_ray = new Ray2D(new Vector3(this_x - 0.51f, this_y, 0), new Vector3(-1, 0, 0));
watch_hit = Physics2D.Raycast(watch_ray.origin, watch_ray.direction);
if (watch_hit.collider.gameObject.name == "Player")
{
LeftDirection();
return true;
}
return false;
}
else if (move_direction == "right")
{
Ray2D watch_ray = new Ray2D(new Vector3(this_x, this_y + 0.51f, 0), new Vector3(0, 1, 0));
RaycastHit2D watch_hit = Physics2D.Raycast(watch_ray.origin, watch_ray.direction);
if (watch_hit.collider.gameObject.name == "Player")
{
UpDirection();
return true;
}
watch_ray = new Ray2D(new Vector3(this_x + 0.51f, this_y, 0), new Vector3(1, 0, 0));
watch_hit = Physics2D.Raycast(watch_ray.origin, watch_ray.direction);
if (watch_hit.collider.gameObject.name == "Player")
{
RightDirection();
return true;
}
watch_ray = new Ray2D(new Vector3(this_x, this_y - 0.51f, 0), new Vector3(0, -1, 0));
watch_hit = Physics2D.Raycast(watch_ray.origin, watch_ray.direction);
if (watch_hit.collider.gameObject.name == "Player")
{
DownDirection();
return true;
}
return false;
}
else if (move_direction == "left")
{
Ray2D watch_ray = new Ray2D(new Vector3(this_x, this_y + 0.51f, 0), new Vector3(0, 1, 0));
RaycastHit2D watch_hit = Physics2D.Raycast(watch_ray.origin, watch_ray.direction);
if (watch_hit.collider.gameObject.name == "Player")
{
UpDirection();
return true;
}
watch_ray = new Ray2D(new Vector3(this_x, this_y - 0.51f, 0), new Vector3(0, -1, 0));
watch_hit = Physics2D.Raycast(watch_ray.origin, watch_ray.direction);
if (watch_hit.collider.gameObject.name == "Player")
{
DownDirection();
return true;
}
watch_ray = new Ray2D(new Vector3(this_x - 0.51f, this_y, 0), new Vector3(-1, 0, 0));
watch_hit = Physics2D.Raycast(watch_ray.origin, watch_ray.direction);
if (watch_hit.collider.gameObject.name == "Player")
{
LeftDirection();
return true;
}
return false;
}
return false;
}
public List<bool> PlayerDistanceCheck()
{
// 位置情報
player_left = player_x < this_x;
player_down = player_y < this_y;
List<bool> distance_result = new List<bool>() { player_left, player_down };
return distance_result;
}
Dictionary<string, bool> MoveAvailabeleDistance()
{
Ray2D up_ray = new Ray2D(new Vector3(this_x, this_y + 0.51f, 0), new Vector3(0, 1, 0));
RaycastHit2D up_hit = Physics2D.Raycast(up_ray.origin, up_ray.direction, 0.6f);
Ray2D down_ray = new Ray2D(new Vector3(this_x, this_y - 0.51f, 0), new Vector3(0, -1, 0));
RaycastHit2D down_hit = Physics2D.Raycast(down_ray.origin, down_ray.direction, 0.6f);
Ray2D right_ray = new Ray2D(new Vector3(this_x + 0.51f, this_y, 0), new Vector3(1, 0, 0));
RaycastHit2D right_hit = Physics2D.Raycast(right_ray.origin, right_ray.direction, 0.6f);
Ray2D left_ray = new Ray2D(new Vector3(this_x - 0.51f, this_y, 0), new Vector3(-1, 0, 0));
RaycastHit2D left_hit = Physics2D.Raycast(left_ray.origin, left_ray.direction, 0.6f);
//Debug.DrawRay(up_ray.origin, up_ray.direction * 0.2f, Color.red, 1f, false);
//Debug.DrawRay(down_ray.origin, down_ray.direction * 0.2f, Color.red, 1f, false);
//Debug.DrawRay(right_ray.origin, right_ray.direction * 0.2f, Color.red, 1f, false);
//Debug.DrawRay(left_ray.origin, left_ray.direction * 0.2f, Color.red, 1f, false);
var moving_available = new Dictionary<string, bool>();
if (up_hit)
{
if (LayerMask.LayerToName(up_hit.collider.gameObject.layer) == "Enemy" || LayerMask.LayerToName(up_hit.collider.gameObject.layer) == "Obstract")
{
bool up_hit_bool = true;
moving_available.Add("up", up_hit_bool);
}
else
{
bool up_hit_bool = false;
moving_available.Add("up", up_hit_bool);
}
}
else
{
bool up_hit_bool = false;
moving_available.Add("up", up_hit_bool);
}
if (down_hit)
{
if (LayerMask.LayerToName(down_hit.collider.gameObject.layer) == "Enemy" || LayerMask.LayerToName(down_hit.collider.gameObject.layer) == "Obstract")
{
bool down_hit_bool = true;
moving_available.Add("down", down_hit_bool);
}
else
{
bool down_hit_bool = false;
moving_available.Add("down", down_hit_bool);
}
}
else
{
bool down_hit_bool = false;
moving_available.Add("down", down_hit_bool);
}
if (right_hit)
{
if (LayerMask.LayerToName(right_hit.collider.gameObject.layer) == "Enemy" || LayerMask.LayerToName(right_hit.collider.gameObject.layer) == "Obstract")
{
bool right_hit_bool = true;
moving_available.Add("right", right_hit_bool);
}
else
{
bool right_hit_bool = false;
moving_available.Add("right", right_hit_bool);
}
}
else
{
bool right_hit_bool = false;
moving_available.Add("right", right_hit_bool);
}
if (left_hit)
{
if (LayerMask.LayerToName(left_hit.collider.gameObject.layer) == "Enemy" || LayerMask.LayerToName(left_hit.collider.gameObject.layer) == "Obstract")
{
bool left_hit_bool = true;
moving_available.Add("left", left_hit_bool);
}
else
{
bool left_hit_bool = false;
moving_available.Add("left", left_hit_bool);
}
}
else
{
bool left_hit_bool = false;
moving_available.Add("left", left_hit_bool);
}
return moving_available;
}
public void UpDirection()
{
move_target = move_target + new Vector3(0f, 1f, 0f);
move_direction = "up";
EnemyrAnim.SetFloat("X", 0);
EnemyrAnim.SetFloat("Y", 1);
}
public void DownDirection()
{
move_target = move_target + new Vector3(0f, -1f, 0f);
move_direction = "down";
EnemyrAnim.SetFloat("X", 0);
EnemyrAnim.SetFloat("Y", -1);
}
public void RightDirection()
{
move_target = move_target + new Vector3(1f, 0f, 0f);
move_direction = "right";
EnemyrAnim.SetFloat("X", 1);
EnemyrAnim.SetFloat("Y", 0);
}
public void LeftDirection()
{
move_target = move_target + new Vector3(-1f, 0f, 0f);
move_direction = "left";
EnemyrAnim.SetFloat("X", -1);
EnemyrAnim.SetFloat("Y", 0);
}
public virtual void OnCollisionEnter2D(Collision2D collision)
{
Debug.Log(LayerMask.LayerToName(collision.gameObject.layer));
if (LayerMask.LayerToName(collision.gameObject.layer) == "Enemy")
{
if (move_direction == "up")
{
DownDirection();
}
else if (move_direction == "down")
{
UpDirection();
}
else if (move_direction == "left")
{
RightDirection();
}
else if (move_direction == "right")
{
LeftDirection();
}
}
else if(LayerMask.LayerToName(collision.gameObject.layer) == "Obstract")
{
if (move_direction == "up")
{
DownDirection();
}
else if (move_direction == "down")
{
UpDirection();
}
else if (move_direction == "left")
{
RightDirection();
}
else if (move_direction == "right")
{
LeftDirection();
}
}
else if (LayerMask.LayerToName(collision.gameObject.layer) == "Player")
{
if(this.gameObject.name.Contains("Ghost") == false)
{
rb.velocity = Vector2.zero;
}
GameObject[] enemy_objs = GameObject.FindGameObjectsWithTag("Enemy");
foreach (GameObject enemy_obj in enemy_objs)
{
EnemyController script = enemy_obj.GetComponent<EnemyController>();
script.moveSpeed = 0;
if (enemy_obj.name.Contains("Ghost"))
{
GhostController ghost_script = enemy_obj.GetComponent<GhostController>();
ghost_script.EventWait = true;
}
Animator anim = enemy_obj.GetComponent<Animator>();
anim.enabled = false;
}
PlayerController.instance.EnemyHit();
}
}
}
コードをそのまま引っ張ってきたので、解説外の部分もありますが、機会があれば、別で解説したいと思います。
おわり
実際のゲームではキャラクターごとの特殊な動きも設定していて、モンスターの特徴を表すことにも挑戦しています(よければunityroomのゲームを遊んで確かめてみてください)。ただ、基本的な動作としては上に書いたプログラムで動いていて、個別の動きはモンスターごとにclassを継承してそれぞれ調整するようにしていました。
何気なく遊んでいると意識しないんですが、ゲームキャラクターの動き一つとっても考えなければいけないことが多く、個人的にはとても勉強になる経験でした。読んでいただき、ありがとうございました。
コメント