智能巡逻兵
- 提交要求:
- 游戏设计要求:
- 创建一个地图和若干巡逻兵(使用动画);
- 每个巡逻兵走一个3~5个边的凸多边型,位置数据是相对地址。即每次确定下一个目标位置,用自己当前位置为原点计算;
- 巡逻兵碰撞到障碍物,则会自动选下一个点为目标;
- 巡逻兵在设定范围内感知到玩家,会自动追击玩家;
- 失去玩家目标后,继续巡逻;
- 计分:玩家每次甩掉一个巡逻兵计一分,与巡逻兵碰撞游戏结束;
- 程序设计要求:
- 必须使用订阅与发布模式传消息
- subject:OnLostGoal
- Publisher: GameEventManager
- Subscriber: FirstSceneController
- 必须使用订阅与发布模式传消息
本次实验作业我参考了往年师兄的代码,借用了师兄们选用过的Ybot作为Player和Guard的模型。为了增加游戏的趣味性,我在游戏中加入了胜利条件:即玩家需要走到迷宫的尽头去拿到NPC要求的宝藏之后再原路返回送给NPC,若在路上被侦察兵发现则游戏失败。此外,为了增加游戏的收集要素,我增加了一个“装备”在地图的右上角,可抵消一次被侦察兵发现的次数。
源代码:UnityGame/AutomaticGuard at master · ShirohaBili/UnityGame (github.com)
演示视频:Assignment7 智能巡逻兵
因为本次实验代码量有点大,因此我就挑选一些我认为比较重要的代码进行讲解:
Animator
这里我只设置了按空格可以有一个翻滚的动作,本来是设置了一个跳跃的,但因为跳跃会出问题,因此我又把它删掉了。实际上似乎只用跑的也行
PlayerCollide
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class PlayerCollide : MonoBehaviour {
private IUserAction action = SSDirector.GetInstance().CurrentScenceController as IUserAction;
void OnCollisionEnter(Collision other) {
//当玩家与侦察兵相撞
if (other.gameObject.tag == "Guard") {
if (action.GetEquipment()) {
// Destroy(other.gameObject);
action.setEquipment(false);
}
else Singleton<GameEventManager>.Instance.PlayerGameover();
}
//当玩家捡到宝藏时
if(other.gameObject.tag == "Treasure"){
Destroy(other.gameObject);
Singleton<GameEventManager>.Instance.PlayerWining();
}
//玩家交付宝藏给NPC,任务胜利
if (other.gameObject.tag == "NPC"){
if (action.GetWinning()) Singleton<GameEventManager>.Instance.TreasureReceive();
}
//玩家获取装备
if(other.gameObject.tag == "Equipment"){
Destroy(other.gameObject);
Singleton<GameEventManager>.Instance.EquipmentReceive();
}
}
}
这一段代码利用消息订阅/发布的模式,实现了对游戏内各种要素的碰撞检验,即巡逻兵、NPC、宝物和装备。
GameEventManager
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class GameEventManager : MonoBehaviour {
public delegate void ScoreEvent();
public static event ScoreEvent ScoreChange;
public delegate void GameoverEvent();
public static event GameoverEvent GameoverChange;
public delegate void GameWiningEvent();
public static event GameWiningEvent GameWiningChange;
public delegate void TreasureReceiveEvent();
public static event TreasureReceiveEvent TreasureReceiveChange;
public delegate void EquipmentReceiveEvent();
public static event EquipmentReceiveEvent EquipmentReceiveChange;
public void PlayerEscape() {
if (ScoreChange != null) {
ScoreChange();
}
}
public void PlayerGameover(){
if (GameoverChange != null) {
GameoverChange();
}
}
public void PlayerWining(){
if (GameWiningChange !=null){
GameWiningChange();
}
}
public void TreasureReceive(){
if (TreasureReceiveChange !=null){
TreasureReceiveChange();
}
}
public void EquipmentReceive(){
if (EquipmentReceiveChange != null){
EquipmentReceiveChange();
}
}
}
这一段代码实现了对所有游戏事件的代理机制。
Camera
我分别写了两个脚本来控制相机,一个控制相机跟着玩家模型移动,另一个控制相机根据玩家左右移动鼠标而移动
CameraController
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class CameraConrtoller : MonoBehaviour {
public PlayerInput pi;
public float horizontalSpeed = 100f;
public float verticalSpeed = 80f;
public float cameraDampValue = 0.5f;
private GameObject playerHandle;
private GameObject cameraHandle;
private float tempEulerX;
private GameObject model;
private GameObject camera;
private Vector3 cameraDampVelocity;
void Awake() {
cameraHandle = transform.parent.gameObject;
playerHandle = cameraHandle.transform.parent.gameObject;
model = playerHandle.GetComponent<ActorController>().model;
camera = Camera.main.gameObject;
tempEulerX = 20f;
}
// Update is called once per frame
void FixedUpdate() {
Vector3 tempModelEuler = model.transform.eulerAngles;
playerHandle.transform.Rotate(Vector3.up, pi.Jright * horizontalSpeed * Time.fixedDeltaTime);
tempEulerX -= pi.Jup * verticalSpeed * Time.fixedDeltaTime;
tempEulerX = Mathf.Clamp(tempEulerX, -35, 30);
cameraHandle.transform.localEulerAngles = new Vector3(tempEulerX, 0, 0);
model.transform.eulerAngles = tempModelEuler;
camera.transform.position = Vector3.SmoothDamp(
camera.transform.position, transform.position,
ref cameraDampVelocity, cameraDampValue);
camera.transform.eulerAngles = transform.eulerAngles;
}
}
CameraMove
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class CameraMove : MonoBehaviour
{
public Transform target; //相机追随目标
public float xSpeed = 200; //X轴方向拖动速度
public float ySpeed = 200; //Y轴方向拖动速度
public float mSpeed = 10; //放大缩小速度
public float yMinLimit = -50; //在Y轴最小移动范围
public float yMaxLimit = 50; //在Y轴最大移动范围
public float distance = 2; //相机视角距离
public float minDinstance = 2; //相机视角最小距离
public float maxDinstance = 10; //相机视角最大距离
public float x = 0.0f;
public float y = 0.0f;
public float damping = 5.0f;
public bool needDamping = true;
public bool lockCursor = true;
private bool m_cursorIsLocked = true;
// Start is called before the first frame update
void Start()
{
target = transform.parent.parent.gameObject.transform;
Vector3 angle = transform.eulerAngles;
x = angle.y;
y = angle.x;
}
// Update is called once per frame
void LateUpdate()
{
if (target)
{
// if (Input.GetMouseButton(1))
// {
x += Input.GetAxis("Mouse X") * xSpeed * 0.02f;
y -= Input.GetAxis("Mouse Y") * ySpeed * 0.02f;
y = ClamAngle(y, yMinLimit, yMaxLimit);
// }
distance -= Input.GetAxis("Mouse ScrollWheel") * mSpeed;
distance = Mathf.Clamp(distance, minDinstance, maxDinstance);
Quaternion rotation = Quaternion.Euler(y, x, 0.0f);
Vector3 disVector = new Vector3(0.0f, 0.0f, -distance);
Vector3 position = rotation * disVector + target.position;
if (needDamping)
{
transform.rotation = Quaternion.Lerp(transform.rotation, rotation, Time.deltaTime * damping);
transform.position = Vector3.Lerp(transform.position, position, Time.deltaTime * damping);
}
else
{
transform.rotation = rotation;
transform.position = position;
}
UpdateCursorLock();
}
}
static float ClamAngle(float angle, float min, float max)
{
if (angle < -360)
{
angle += 360;
}
if(angle > 360)
{
angle -= 360;
}
return Mathf.Clamp(angle, min, max);
}
public void SetCursorLock(bool value)
{
lockCursor = value;
if(!lockCursor)
{//we force unlock the cursor if the user disable the cursor locking helper
Cursor.lockState = CursorLockMode.None;
Cursor.visible = true;
}
}
public void UpdateCursorLock()
{
//if the user set "lockCursor" we check & properly lock the cursos
if (lockCursor)
InternalLockUpdate();
}
private void InternalLockUpdate()
{
if(Input.GetKeyUp(KeyCode.Escape))
{
m_cursorIsLocked = false;
}
else if(Input.GetMouseButtonUp(0))
{
m_cursorIsLocked = true;
}
if (m_cursorIsLocked)
{
Cursor.lockState = CursorLockMode.Locked;
Cursor.visible = false;
}
else if (!m_cursorIsLocked)
{
Cursor.lockState = CursorLockMode.None;
Cursor.visible = true;
}
}
}
控制相机移动的方法相对简单一些,只需要设置一下跟随的目标,使用平滑的跟踪方式进行跟踪即可。控制相机旋转的方法相对复杂,我这里设置了拖动的最大上限,防止角度过大会出现的问题,并设置了可以利用滚轮调节相机的距离,便于玩家选择适合自己的视距。同时设计了隐藏鼠标指针的方法,点击画面隐藏鼠标指针,按ESC键可重新显示鼠标指针。
GuardActionManager
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class GuardActionManager : SSActionManager, ISSActionCallback {
private GuardPatrolAction patrol;
private GameObject player;
public void GuardPatrol(GameObject guard, GameObject _player) {
player = _player;
patrol = GuardPatrolAction.GetSSAction(guard.transform.position);
this.RunAction(guard, patrol, this);
}
public void SSActionEvent(
SSAction source, SSActionEventType events = SSActionEventType.Competeted,
int intParam = 0, GameObject objectParam = null) {
if (intParam == 0) {
//追逐
GuardFollowAction follow = GuardFollowAction.GetSSAction(player);
this.RunAction(objectParam, follow, this);
} else {
//巡逻
GuardPatrolAction move = GuardPatrolAction.GetSSAction(objectParam.gameObject.GetComponent<GuardData>().start_position);
this.RunAction(objectParam, move, this);
Singleton<GameEventManager>.Instance.PlayerEscape();
}
}
}
巡逻兵察觉到玩家进来就变成追逐模式,玩家逃离则继续巡逻。
GuardPatrol
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class GuardPatrolAction : SSAction {
private enum Dirction { EAST, NORTH, WEST, SOUTH };
private float pos_x, pos_z;
private float move_length;
private bool move_sign = true;
private Dirction dirction = Dirction.EAST;
private GuardData data;
private Animator anim;
private Rigidbody rigid;
private Vector3 planarVec; // 平面移动向量
private GuardPatrolAction() { }
public override void Start() {
data = gameobject.GetComponent<GuardData>();
anim = gameobject.GetComponent<Animator>();
rigid = gameobject.GetComponent<Rigidbody>();
//播放走路动画
anim.SetFloat("forward", 1.0f);
}
public static GuardPatrolAction GetSSAction(Vector3 location) {
GuardPatrolAction action = CreateInstance<GuardPatrolAction>();
action.pos_x = location.x;
action.pos_z = location.z;
//设定移动矩形的边长
action.move_length = Random.Range(5, 6);
return action;
}
public override void Update() {
//保留供物理引擎调用
planarVec = gameobject.transform.forward * data.walkSpeed;
}
public override void FixedUpdate() {
//巡逻
Gopatrol();
//玩家进入该区域,巡逻结束,开始追逐
if (data.playerSign == data.sign) {
this.destroy = true;
this.callback.SSActionEvent(this, SSActionEventType.Competeted, 0, this.gameobject);
}
}
void Gopatrol() {
if (move_sign) {
//不需要转向则设定一个目的地,按照矩形移动
switch (dirction) {
case Dirction.EAST:
pos_x -= move_length;
break;
case Dirction.NORTH:
pos_z += move_length;
break;
case Dirction.WEST:
pos_x += move_length;
break;
case Dirction.SOUTH:
pos_z -= move_length;
break;
}
move_sign = false;
}
this.transform.LookAt(new Vector3(pos_x, 0, pos_z));
float distance = Vector3.Distance(transform.position, new Vector3(pos_x, 0, pos_z));
if (distance > 0.9) {
rigid.velocity = new Vector3(planarVec.x, rigid.velocity.y, planarVec.z);
} else {
dirction = dirction + 1;
if(dirction > Dirction.SOUTH) {
dirction = Dirction.EAST;
}
move_sign = true;
}
}
}
这一脚本控制巡逻兵的动作,按题目要求,设置巡逻兵按一个矩形移动,但或许是因为Ybot自带的动画的问题,巡逻兵会卡在一个地方不动一段时间,而后恢复正常。
GuardFactory
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class GuardFactory : MonoBehaviour {
private GameObject guard = null; //巡逻兵
private List<GameObject> used = new List<GameObject>(); //正在使用的巡逻兵列表
private Vector3[] vec = new Vector3[9]; //每个巡逻兵的初始位置
public List<GameObject> GetPatrols() {
int[] pos_x = { -6, 4, 13 };
int[] pos_z = { -4, 6, -13 };
int index = 0;
for(int i=0;i < 3;i++) {
for(int j=0;j < 3;j++) {
vec[index] = new Vector3(pos_x[i], 0, pos_z[j] + 5);
index++;
}
}
for(int i = 0; i < 8; i++) {
guard = Instantiate(Resources.Load<GameObject>("Prefabs/Guard"));
guard.transform.position = vec[i];
guard.GetComponent<GuardData>().sign = i + 1;
guard.GetComponent<GuardData>().start_position = vec[i];
guard.GetComponent<Animator>().SetFloat("forward", 1);
used.Add(guard);
}
return used;
}
}
沿用了前几个作业中的工厂模式,设置了一个巡逻兵工厂,这里需要说明的是,sign变量的设置是为了检验玩家是否和巡逻兵在一个区域内,若在,则追逐,不在则巡逻。
FirstSceneController
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;
public class FirstSceneController : MonoBehaviour, IUserAction, ISceneController {
public GuardFactory guard_factory; //巡逻者工厂
public ScoreRecorder recorder; //记录员
public GuardActionManager action_manager; //运动管理器
public int playerSign = -1; //当前玩家所处哪个格子
public GameObject player; //玩家
public UserGUI gui; //交互界面
public GameObject treasure;
public GameObject npc;
public GameObject equipment;
private List<GameObject> guards; //场景中巡逻者列表
private bool game_over = false; //游戏结束
private bool Wining = false;
private bool receive = false;
private bool equipmentRec = false;
private bool equipmentGet = false;
void Awake() {
SSDirector director = SSDirector.GetInstance();
director.CurrentScenceController = this;
guard_factory = Singleton<GuardFactory>.Instance;
action_manager = gameObject.AddComponent<GuardActionManager>() as GuardActionManager;
gui = gameObject.AddComponent<UserGUI>() as UserGUI;
LoadResources();
recorder = Singleton<ScoreRecorder>.Instance;
}
void Update() {
for (int i = 0; i < guards.Count; i++) {
guards[i].gameObject.GetComponent<GuardData>().playerSign = playerSign;
}
}
public void LoadResources() {
Instantiate(Resources.Load<GameObject>("Prefabs/Plane"));
player = Instantiate(
Resources.Load("Prefabs/Player"),
new Vector3(10, 0, -10), Quaternion.identity) as GameObject;
treasure = Instantiate(Resources.Load("Prefabs/treasure"), new Vector3(0f,0.33f,-19f), Quaternion.identity) as GameObject;
npc = Instantiate(Resources.Load("Prefabs/NPC"), new Vector3(12f,0f,-12f),Quaternion.identity) as GameObject;
equipment = Instantiate(Resources.Load("Prefabs/equipment"), new Vector3(9f,1f,10f),Quaternion.identity) as GameObject;
guards = guard_factory.GetPatrols();
for (int i = 0; i < guards.Count; i++) {
action_manager.GuardPatrol(guards[i], player);
}
}
public int GetScore() {
return recorder.GetScore();
}
public bool GetGameover() {
return game_over;
}
public bool GetWinning(){
return Wining;
}
public bool GetReceive(){
return receive;
}
public bool GetEquipment(){
return equipmentRec;
}
public bool HadEquipment(){
return equipmentGet;
}
public void Restart() {
SceneManager.LoadScene("Scenes/mySence");
}
void OnEnable() {
GameEventManager.ScoreChange += AddScore;
GameEventManager.GameoverChange += Gameover;
GameEventManager.GameWiningChange += GameWining;
GameEventManager.TreasureReceiveChange += ReceiveChange;
GameEventManager.EquipmentReceiveChange += EquipmentChange;
}
void OnDisable() {
GameEventManager.ScoreChange -= AddScore;
GameEventManager.GameoverChange -= Gameover;
GameEventManager.GameWiningChange -= GameWining;
GameEventManager.TreasureReceiveChange -= ReceiveChange;
GameEventManager.EquipmentReceiveChange -= EquipmentChange;
}
void AddScore() {
recorder.AddScore();
}
void Gameover() {
game_over = true;
}
void GameWining(){
Wining = true;
}
void ReceiveChange(){
receive = true;
}
void EquipmentChange(){
equipmentRec = true;
equipmentGet = true;
}
public void setEquipment(bool status){
equipmentRec = status;
}
}
本脚本充当了整个游戏项目的初始化管理者的角色,同时也担当了订阅/发布模式中的接收者的角色,管理着本游戏的全部游戏事件。
剩下的一些脚本如FSM,UserGUI等不是特别难理解,因此我没有将它列出来,若有需要的话,请自行翻阅源代码。