智能巡逻兵
游戏视频 https://www.bilibili.com/video/av73240950/
项目地址 https://gitee.com/jenny_s/3DIntelligentPatrol
游戏设计要求:
-
创建一个地图和若干巡逻兵(使用动画);
-
每个巡逻兵走一个3~5个边的凸多边型,位置数据是相对地址。即每次确定下一个目标位置,用自己当前位置为原点计算;
-
巡逻兵碰撞到障碍物,则会自动选下一个点为目标;
-
巡逻兵在设定范围内感知到玩家,会自动追击玩家;
-
失去玩家目标后,继续巡逻;
-
计分:玩家每次甩掉一个巡逻兵计一分,与巡逻兵碰撞游戏结束;
-
程序设计要求:
- 必须使用订阅与发布模式传消息
- subject:OnLostGoal
- Publisher: ?
- Subscriber: ?
- 工厂模式生产巡逻兵
- 必须使用订阅与发布模式传消息
游戏设计
本次游戏使用了Unity的Animator控件,给人物增加了动作,使得游戏更具有可玩性和观赏感。对角色增加了前进、后退、左拐、右拐和逗留的动作,通过wasd
键来控制事件的触发和转移。
// 单例模式,全局唯一角色
public class Player
{
private static Player _instance; //Player类的实例
public static Player GetInstance()
{
if (_instance == null)
{
_instance = new Player();
}
return _instance;
}
private Animator animator;
private AnimatorStateInfo stateinfo;
GameObject Role;
private Player()
{
Role = GameObject.FindWithTag("Player");
animator = Role.GetComponent<Animator>();
}
private void setStateFalse()
{
animator.SetBool("Idle", false);
animator.SetBool("Forward", false);
animator.SetBool("Left", false);
animator.SetBool("Right", false);
animator.SetBool("Backward", false);
}
public void idle()
{
if(animator != null)
{
setStateFalse();
animator.SetBool("Idle", true);
}
}
public void forward()
{
if (animator != null)
{
setStateFalse();
animator.SetBool("Forward", true);
}
}
public void backward()
{
if (animator != null)
{
}
setStateFalse();
animator.SetBool("Backward", true);
}
public void left()
{
if (animator != null)
{
setStateFalse();
animator.SetBool("Left", true);
}
}
public void right()
{
if (animator != null)
{
setStateFalse();
animator.SetBool("Right", true);
}
}
}
巡逻兵有走路、奔跑、跳跃和攻击四种动作,当玩家没有进入巡逻兵的视野范围内时,巡逻兵在一定区域内走路巡逻;当玩家进入巡逻兵的视野范围内时,巡逻兵根据玩家的位置进行追击;当巡逻兵追击到玩家时,游戏结束。
巡逻兵的视野范围是一个Cube触发器,而巡逻兵的攻击范围是一个Cube碰撞器,如下图所示,外面的立方体是触发器,里面的立方体是碰撞器。
为了保证巡逻兵在追击的时候能够“面向”玩家,还需要计算一个偏转角度,如下所示。
Vector3 rolepos = role.transform.position;
float offX = (-rolepos.x + transform.position.x) * Time.deltaTime;
float offZ = (-rolepos.z + transform.position.z) * Time.deltaTime;
transform.Translate(offX, 0, offZ);
if(offZ != 0)
{
float angle = Mathf.Atan(Mathf.Abs(offX / offZ))*Time.deltaTime;
transform.Rotate(new Vector3(0, angle, 0));
}
挂载在巡逻兵上的运动脚本如下所示:
public class Soldier : MonoBehaviour
{
private Animator animator;
private AnimatorStateInfo stateinfo;
GameObject soldier;
private int SoldierState = 0; // Walk=0,Run and Follow=1, hit = 2
GameObject role;
private void setStateFalse()
{
animator.SetBool("Walk", false);
animator.SetBool("Run", false);
animator.SetBool("Jump", false);
}
// Start is called before the first frame update
void Start()
{
soldier = this.transform.gameObject;
role = GameObject.FindWithTag("Player");
animator = soldier.GetComponent<Animator>();
SoldierState = 0;
setStateFalse();
animator.SetBool("Walk", true);
}
// Update is called once per frame
void Update()
{
if (SoldierState == 0)
{
float translationZ = 2f;
translationZ *= Time.deltaTime;
transform.Translate(0, 0, translationZ);
}
else if(SoldierState == 1)
{
Vector3 rolepos = role.transform.position;
float offX = (-rolepos.x + transform.position.x) * Time.deltaTime;
float offZ = (-rolepos.z + transform.position.z) * Time.deltaTime;
transform.Translate(offX, 0, offZ);
if(offZ != 0)
{
float angle = Mathf.Atan(Mathf.Abs(offX / offZ))*Time.deltaTime;
transform.Rotate(new Vector3(0, angle, 0));
}
}
}
private void OnTriggerEnter(Collider other)
{
}
// 当角色停留在一个monster的触发区域内时,monster追逐角色
private void OnTriggerStay(Collider other)
{
if (other.gameObject.tag == "Player" && SoldierState == 0)
{
Debug.Log("stay");
setStateFalse();
animator.SetBool("Run", true);
SoldierState = 1;
}
}
// 当角色离开一个monster的触发区域内时,monster恢复巡逻状态
private void OnTriggerExit(Collider other)
{
if (other.gameObject.tag == "Player" && SoldierState == 1)
{
Debug.Log("stay");
setStateFalse();
animator.SetBool("Walk", true);
SoldierState = 0;
}
}
public void TurnRight()
{
transform.Rotate(0, 90, 0);
}
private void OnCollisionEnter(Collision collision)
{
if (collision.gameObject.tag == "Wall")
{
Debug.Log("WALL");
TurnRight();
}
else if (collision.gameObject.tag == "Player")
{
Debug.Log("player");
setStateFalse();
animator.SetBool("Jump", true);
SoldierState = 2;
//soldier.GetComponent<Rigidbody>().isKinematic = true;
role.GetComponent<Rigidbody>().isKinematic = true;
Singleton<MyGUI>.Instance.state = 1;
}
}
}
工厂模式
这里采用单例工厂生产巡逻兵。在FirstSceneController
中加入单例工厂,然后使用该工厂生产的巡逻兵。
this.gameObject.AddComponent<SoldierFactory>();
兵工厂用来负责巡逻兵的生产和销毁。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class SoldierFactory : MonoBehaviour
{
// Start is called before the first frame update
private List<GameObject> used = new List<GameObject>();
private List<GameObject> free = new List<GameObject>();
// 巡逻兵的“出生”位置
Vector3 []MonsterPos = {
new Vector3(7, 0, 17) , new Vector3(-7, 0, 17),
new Vector3(-7, 0, 34) , new Vector3(12, 0, 36)
};
public GameObject GetSoldier(int index)
{
GameObject newSoldier = null;
if (free.Count > 0)
{
newSoldier = free[0];
free.Remove(free[0]);
}
else
{
newSoldier = GameObject.Instantiate<GameObject>(Resources.Load<GameObject>("Prefabs/Monster"));
}
newSoldier.transform.position = MonsterPos[index];
used.Add(newSoldier);
newSoldier.name = newSoldier.GetInstanceID().ToString();
newSoldier.SetActive(true);
return newSoldier;
}
public void FreeSoldier(GameObject soldier)
{
if (soldier != null)
{
used.Remove(soldier);
soldier.SetActive(false);
Destroy(soldier);
}
}
}
观察者模式
我们采用观察者模式(订阅与发布模式)完成如下操作:
- 当角色成功在一个方形区域内甩掉巡逻兵后,会进行记分;
在这里,发布者是不同区域间的“门洞”,观察者是记分系统。
发布者(Subject)是SceneTrigger
。
public class SceneTrigger : MonoBehaviour
{
protected List<Observer> observers = new List<Observer>();
public void attach(Observer o)
{
observers.Add(o);
}
public void detach(Observer o)
{
observers.Remove(o);
}
public void notify()
{
foreach (Observer o in observers)
{
o.update();
}
}
// 当角色离开一个方块区域时,加一分
private void OnTriggerExit(Collider other)
{
if (other.gameObject.tag == "Player" )
{
notify();
}
}
}
观察者们要实现统一的观察者接口。
public interface Observer
{
void update();
}
计分器是观察者。
public class ScoreController : Observer
{
public int score;
// Start is called before the first frame update
void Start()
{
score = 0;
}
public void reset()
{
score = 0;
}
public void update()
{
score++;
}
public int getScore()
{
return score;
}
}
游戏效果
游戏心得
本次作业做了三天的时间,耗费了不少精力,但是感觉本人对设计模式的理解和掌握依旧不够,有很多代码冗余,希望下一次能够更进一步。