作业与练习
游戏设计要求
- 创建一个地图和若干巡逻兵(使用动画);
- 每个巡逻兵走一个3~5个边的凸多边型,位置数据是相对地址。即每次确定下一个目标位置,用自己当前位置为原点计算;
- 巡逻兵碰撞到障碍物,则会自动选下一个点为目标;
- 巡逻兵在设定范围内感知到玩家,会自动追击玩家;
- 失去玩家目标后,继续巡逻;
- 计分:玩家每次甩掉一个巡逻兵计一分,与巡逻兵碰撞游戏结束;
程序设计要求:
- 必须使用订阅与发布模式传消息
- 工厂模式生产巡逻兵
实践内容
本次架构的基础还是沿用了之前的脚本架构,除了中间修改了一点之外,其余为变动。另外,根据题目要求,重点放在了预制动画的制作以及观察者模式的设计上。
场景及游戏效果图
- 场景
- 游戏界面
预制的设计
Player的设计
组件的设计
添加的组件是:刚体、碰撞器、动画管理者以及管理的脚本。
刚体的作用是使其运动符合物理学。
碰撞器是为了实现与巡逻兵、墙、触发器的碰撞。动画管理者的设计
首先是三个参数的说明,bool类型的isLive是表示角色是否存活,Speed是角色运动的速度,toDie是表示角色是否死亡。
Anystate在isDie满足的情况下,将会实现die的操作。
idle和run是角色静止跟跑步时的切换(由于商店日常抽风,未能找到合适的预制,因此该预制没有相关的运动动画,有点僵尸)脚本的设计
挂在预制上的脚本如下:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
[RequireComponent(typeof(Animator))]
[RequireComponent(typeof(CapsuleCollider))]
[RequireComponent(typeof(Rigidbody))]
public class ActionController : MonoBehaviour {
private Animator ani;
private AnimatorStateInfo currentState;
private Rigidbody rig;
//用来得到物体本身的组件,再对组件进行相关的操作
private Vector3 velocity;
private float rotateSpeed = 15f; //旋转速度
private float runSpeed = 5f; //奔跑速度
// Use this for initialization
void Start () {
Debug.Log("233");
ani = GetComponent<Animator>();
rig = GetComponent<Rigidbody>();
}
private void FixedUpdate()
{
if (!ani.GetBool("isLive")) return;
//死亡不进行任何动作
float x = Input.GetAxis("Horizontal");
float z = Input.GetAxis("Vertical");
ani.SetFloat("Speed", Mathf.Max(Mathf.Abs(x), Mathf.Abs(z)));
//设置速度
ani.speed = 1 + ani.GetFloat("Speed") / 3;
//???
velocity = new Vector3(x, 0, z);
//如果在运动中输入,则转向
if(x != 0 || z != 0)
{
Quaternion rotation = Quaternion.LookRotation(velocity);
if(transform.rotation != rotation)
{
transform.rotation = Quaternion.Slerp(transform.rotation, rotation, Time.fixedDeltaTime * rotateSpeed);
}
}
this.transform.position += velocity * Time.fixedDeltaTime * runSpeed;
}
//检测是否进入某一区域
private void OnTriggerEnter(Collider other)
{
if(other.gameObject.CompareTag("Area"))
{
Subject publish = Publisher.getInstance();
int patrolType = other.gameObject.name[other.gameObject.name.Length - 1] - '0';
publish.notify(StateOfActor.ENTER_AREA, patrolType, this.gameObject);
//发布消息
}
}
//检测死亡
private void OnCollisionEnter(Collision collision)
{
if (collision.gameObject.CompareTag("Patrol") && ani.GetBool("isLive"))
{
ani.SetBool("isLive", false);
ani.SetTrigger("toDie");
//执行死亡动作
Subject publish = Publisher.getInstance();
publish.notify(StateOfActor.DEATH, 0, null);
}
}
}
该脚本根据键盘的输入,决定角色的运动状态
Patrol的设计
组件的设计
组件的设计其实跟Player的大同小异,基本相同动画管理者的设计
idle代表静止,walk代表巡逻,run代表追赶玩家,主要是三者之间的切换。
参数也跟Player相似。脚本的设计
首先是巡逻兵静止、巡逻、追赶三个动作的实现
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace Tem.Action
{
public enum SSActionState : int { STARTED, COMPLETED}
public interface ISSActionCallback
{
void SSEventAction(SSAction source, SSActionState events = SSActionState.COMPLETED, int intParam = 0, string strParam = null, Object obj = null);
}
public class SSAction : ScriptableObject
{
public bool enable = true;
public bool destory = false;
public GameObject gameObject { get; set; }
public Transform transform { get; set; }
public ISSActionCallback callback { get; set; }
//只能调用子类的重写函数,否则将会报错
public virtual void Start()
{
throw new System.NotImplementedException("Action Start Error!");
}
public virtual void FixedUpdate()
{
throw new System.NotImplementedException("Physics Action Start Error!");
}
public virtual void Update()
{
throw new System.NotImplementedException("Action Update Error!");
}
}
//???
public class CCSequenceAction : SSAction, ISSActionCallback
{
public List<SSAction> sequence;
public int repeat = -1;
public int start = 0;
public static CCSequenceAction GetSSAction(List<SSAction> _sequence, int _start = 0, int _repead = 1)
{
CCSequenceAction actions = ScriptableObject.CreateInstance<CCSequenceAction>();
actions.sequence = _sequence;
actions.start = _start;
actions.repeat = _repead;
return actions;
}
public override void Start()
{
foreach (SSAction ac in sequence)
{
ac.gameObject = this.gameObject;
ac.transform = this.transform;
ac.callback = this;
ac.Start();
}
}
public override void Update()
{
if (sequence.Count == 0) return;
if (start < sequence.Count) sequence[start].Update();
}
public void SSEventAction(SSAction source, SSActionState events = SSActionState.COMPLETED,
int intParam = 0, string strParam = null, Object objParam = null) //通过对callback函数的调用执行下个动作
{
source.destory = false; // 当前动作不能销毁(有可能执行下一次)
this.start++;
if (this.start >= this.sequence.Count)
{
this.start = 0;
if (this.repeat > 0) repeat--;
if (this.repeat == 0)
{
this.destory = true;
this.callback.SSEventAction(this);
}
}
}
private void OnDestroy()
{
this.destory = true;
}
}
//站立的动作
public class IdleAction : SSAction
{
private float time;
//站立的时间
private Animator ani;
public static IdleAction GetIdleAction(float time, Animator ani)
{
IdleAction currentAction = ScriptableObject.CreateInstance<IdleAction>();
currentAction.time = time;
currentAction.ani = ani;
return currentAction;
}
public override void Start()
{
ani.SetFloat("Speed", 0);
// 进入站立状态
}
public override void Update()
{
if (time == -1) return;
// 永久站立
time -= Time.deltaTime;
// 减去时间
if (time < 0)
{
this.destory = true;
this.callback.SSEventAction(this);
}
}
}
//巡逻时的动作
public class WalkAction : SSAction
{
private float speed;
private Vector3 target;
private Animator ani;
// 移动速度和目标的地点
public static WalkAction GetWalkAction(Vector3 target, float speed, Animator ani)
{
WalkAction currentAction = ScriptableObject.CreateInstance<WalkAction>();
currentAction.speed = speed;
currentAction.target = target;
currentAction.ani = ani;
return currentAction;
}
public override void Start()
{
ani.SetFloat("Speed", 0.5f);
// 进入走路状态
}
public override void Update()
{
Quaternion rotation = Quaternion.LookRotation(target - transform.position);
if (transform.rotation != rotation) transform.rotation = Quaternion.Slerp(transform.rotation, rotation, Time.deltaTime * speed * 5);
// 进行转向,转向目标方向
this.transform.position = Vector3.MoveTowards(this.transform.position, target, speed * Time.deltaTime);
if (this.transform.position == target)
{
this.destory = true;
this.callback.SSEventAction(this);
}
}
}
//追击时的动作
public class RunAction : SSAction
{
private float speed;
private Transform target;
private Animator ani;
// 移动速度和人物的transform
public static RunAction GetRunAction(Transform target, float speed, Animator ani)
{
RunAction currentAction = ScriptableObject.CreateInstance<RunAction>();
currentAction.speed = speed;
currentAction.target = target;
currentAction.ani = ani;
return currentAction;
}
public override void Start()
{
ani.SetFloat("Speed", 1);
// 进入跑步状态
}
public override void Update()
{
Quaternion rotation = Quaternion.LookRotation(target.position - transform.position);
if (transform.rotation != rotation) transform.rotation = Quaternion.Slerp(transform.rotation, rotation, Time.deltaTime * speed * 5);
// 转向
this.transform.position = Vector3.MoveTowards(this.transform.position, target.position, speed * Time.deltaTime);
if (Vector3.Distance(this.transform.position, target.position) < 0.5)
{
this.destory = true;
this.callback.SSEventAction(this);
}
}
}
//动作管理类
public class SSActionManager : MonoBehaviour
{
//用字典来储存相关的指令
private Dictionary<int, SSAction> diction = new Dictionary<int, SSAction>();
private List<SSAction> AddAction = new List<SSAction>();
private List<int> DeleteAction = new List<int>();
protected void Start()
{
}
protected void Update()
{
foreach(SSAction ac in AddAction)
{
diction[ac.GetInstanceID()] = ac;
}
AddAction.Clear();
//将要进行的动作加入到执行的字典中
//将要删除的加到删除列表中
foreach(KeyValuePair<int, SSAction> dic in diction)
{
SSAction ac = dic.Value;
if(ac.destory == true)
{
DeleteAction.Add(ac.GetInstanceID());
}
else if(ac.enable == true) {
ac.Update();
}
}
//将删除列表中的元素进行删除
foreach(int id in DeleteAction)
{
SSAction ac = diction[id];
diction.Remove(id);
DestroyObject(ac);
}
DeleteAction.Clear();
}
//追赶的时候,由于玩家的位置不断变化,因此需要不断地进行更新
public void runAction(GameObject gameObject, SSAction action, ISSActionCallback callback)
{
action.gameObject = gameObject;
action.transform = gameObject.transform;
action.callback = callback;
AddAction.Add(action);
action.Start();
}
}
public class PYActionManager : MonoBehaviour
{
private Dictionary<int, SSAction> dictionary = new Dictionary<int, SSAction>();
private List<SSAction> watingAddAction = new List<SSAction>();
private List<int> watingDelete = new List<int>();
protected void Start()
{
}
protected void FixedUpdate()
{
foreach (SSAction ac in watingAddAction) dictionary[ac.GetInstanceID()] = ac;
watingAddAction.Clear();
// 将待加入动作加入dictionary执行
foreach (KeyValuePair<int, SSAction> dic in dictionary)
{
SSAction ac = dic.Value;
if (ac.destory) watingDelete.Add(ac.GetInstanceID());
else if (ac.enable) ac.FixedUpdate();
}
// 如果要删除,加入要删除的list,否则更新
foreach (int id in watingDelete)
{
SSAction ac = dictionary[id];
dictionary.Remove(id);
DestroyObject(ac);
}
watingDelete.Clear();
// 将deletelist中的动作删除
}
public void runAction(GameObject gameObject, SSAction action, ISSActionCallback callback)
{
action.gameObject = gameObject;
action.transform = gameObject.transform;
action.callback = callback;
watingAddAction.Add(action);
action.Start();
}
}
}
之后是三个动作的切换,同时该脚本也是挂在预制上的:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Tem.Action;
[RequireComponent(typeof(Animator))]
[RequireComponent(typeof(CapsuleCollider))]
[RequireComponent(typeof(Rigidbody))]
public class PatrolUI : SSActionManager, ISSActionCallback, Observer_one
{
public enum ActionState : int { IDLE, WALKLEFT, WALKFORWARD, WALKRIGHT, WALKBACK }
// 各种动作
private Animator ani;
// 动作
private SSAction currentAction;
private ActionState currentState;
// 保证当前只有一个动作
private const float walkSpeed = 1f;
private const float runSpeed = 3f;
// 跑步和走路的速度
// Use this for initialization
new void Start () {
ani = this.gameObject.GetComponent<Animator>();
Subject publisher = Publisher.getInstance();
publisher.add(this);
// 添加事件
currentState = ActionState.IDLE;
idle();
// 开始时,静止状态
}
// Update is called once per frame
new void Update () {
base.Update();
}
//根据传入的参数决定要执行的动作
public void SSEventAction(SSAction source, SSActionState events = SSActionState.COMPLETED, int intParam = 0, string strParam = null, Object objParam = null)
{
currentState = currentState > ActionState.WALKBACK ? ActionState.IDLE : (ActionState)((int)currentState + 1);
// 改变当前状态
switch (currentState)
{
case ActionState.WALKLEFT:
walkLeft();
break;
case ActionState.WALKRIGHT:
walkRight();
break;
case ActionState.WALKFORWARD:
walkForward();
break;
case ActionState.WALKBACK:
walkBack();
break;
default:
idle();
break;
}
// 执行下个动作
}
public void idle()
{
currentAction = IdleAction.GetIdleAction(Random.Range(1, 1.5f), ani);
this.runAction(this.gameObject, currentAction, this);
}
public void walkLeft()
{
Vector3 target = Vector3.left * Random.Range(3, 5) + this.transform.position;
currentAction = WalkAction.GetWalkAction(target, walkSpeed, ani);
this.runAction(this.gameObject, currentAction, this);
}
public void walkRight()
{
Vector3 target = Vector3.right * Random.Range(3, 5) + this.transform.position;
currentAction = WalkAction.GetWalkAction(target, walkSpeed, ani);
this.runAction(this.gameObject, currentAction, this);
}
public void walkForward()
{
Vector3 target = Vector3.forward * Random.Range(3, 5) + this.transform.position;
currentAction = WalkAction.GetWalkAction(target, walkSpeed, ani);
this.runAction(this.gameObject, currentAction, this);
}
public void walkBack()
{
Vector3 target = Vector3.back * Random.Range(3, 5) + this.transform.position;
currentAction = WalkAction.GetWalkAction(target, walkSpeed, ani);
this.runAction(this.gameObject, currentAction, this);
}
//碰到触发器时,执行相反方向的动作
public void turnNextDirection()
{
currentAction.destory = true;
// 销毁当前动作
switch (currentState)
{
case ActionState.WALKLEFT:
currentState = ActionState.WALKRIGHT;
walkRight();
break;
case ActionState.WALKRIGHT:
currentState = ActionState.WALKLEFT;
walkLeft();
break;
case ActionState.WALKFORWARD:
currentState = ActionState.WALKBACK;
walkBack();
break;
case ActionState.WALKBACK:
currentState = ActionState.WALKFORWARD;
walkForward();
break;
}
// 执行相反动作
//更改追赶的位置
}
public void getGoal(GameObject gameobject)
{
currentAction.destory = true;
// 销毁当前动作
currentAction = RunAction.GetRunAction(gameobject.transform, runSpeed, ani);
this.runAction(this.gameObject, currentAction, this);
// 跑向目标方向
}
public void loseGoal()
{
currentAction.destory = true;
// 销毁当前动作
idle();
// 重新进行动作循环
}
public void stop()
{
currentAction.destory = true;
currentAction = IdleAction.GetIdleAction(-1f, ani);
this.runAction(this.gameObject, currentAction, this);
// 永久站立
}
private void OnCollisionEnter(Collision collision)
{
Debug.Log(collision.gameObject.name);
Transform parent = collision.gameObject.transform.parent;
if (parent != null && parent.CompareTag("Wall")) turnNextDirection();
// 撞到墙
}
private void OnTriggerEnter(Collider other)
{
if (other.gameObject.CompareTag("Door")) turnNextDirection();
// 走出巡逻区域
}
public void notified(StateOfActor state, int pos, GameObject actor)
{
if (state == StateOfActor.ENTER_AREA)
{
if (pos == this.gameObject.name[this.gameObject.name.Length - 1] - '0')
getGoal(actor);
// 如果进入自己的区域,进行追击
else loseGoal();
// 如果离开自己的区域,放弃追击
}
else stop();
// 角色死亡,结束动作
}
}
观察者模式
这个本次实验的重点之一,个人感觉跟仙草的事件委托有异曲同工之妙:
(这里本来有个坑,被我修改了,后面会讲)
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System;
public interface Subject
{
void notify(StateOfActor state, int pos, GameObject actor);
//发布函数
void add(Observer_one observer);
//委托添加函数
void delete(Observer_one observer);
//委托取消函数
}
public interface Observer_one
{
void notified(StateOfActor state, int pos, GameObject actor);
//实现接收函数
}
public enum StateOfActor { ENTER_AREA, DEATH }
//状态
public class Publisher : Subject
{
private delegate void ActionUpdate(StateOfActor state, int pos, GameObject actor);
private ActionUpdate updatelist;
//存储状态,委托定义
private static Subject _instance;
// Use this for initialization
public static Subject getInstance()
{
if(_instance == null)
{
_instance = new Publisher();
}
return _instance;
}
public void notify(StateOfActor state, int pos, GameObject actor)
{
if(updatelist != null)
{
updatelist(state, pos, actor);
}
}
//发布函数
public void add(Observer_one observer)
{
updatelist += observer.notified;
}
//委托添加函数
public void delete(Observer_one observer)
{
updatelist -= observer.notified;
}
}
单例模式
这个就不多讲:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Singleton<T> : MonoBehaviour where T:MonoBehaviour{
public static T instance;
public static T Instance
{
get
{
if(instance == null)
{
instance = (T)FindObjectOfType(typeof(T));
if(instance == null)
{
Debug.LogError("An instance of " + typeof(T) + " is needed in the scene, but it not");
}
}
return instance;
}
}
}
情景管理(场记)
主要是管理场景的切换,预制的加载以及其他的连接:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System;
using UnityEngine.UI;
public class SceneController : MonoBehaviour, Observer_one
{
public Text scoreText;
public Text centerText;
private ScoreRecorder record;
private UIcontroller UI;
private Factory fac;
private float[] posx = { -5, 7, -5, 5 };
private float[] posz = { -5, -7, 5, 5 };
//预制加载的位置
// Use this for initialization
void Start () {
record = new ScoreRecorder();
record.scoreText = scoreText;
UI = new UIcontroller();
UI.centerText = centerText;
fac = Singleton<Factory>.Instance;
Subject publisher = Publisher.getInstance();
publisher.add(this);
// 添加事件
LoadResources();
}
private void LoadResources()
{
Instantiate(Resources.Load("prefabs/Ami"), new Vector3(2, 1, -2), Quaternion.Euler(new Vector3(0, 180, 0)));
// 初始化主角
Factory fac = Singleton<Factory>.Instance;
for (int i = 0; i < posx.Length; i++)
{
GameObject patrol = fac.setOnPos(new Vector3(posx[i], 0, posz[i]), Quaternion.Euler(new Vector3(0, 180, 0)));
patrol.name = "Patrol" + (i + 1);
// 初始化巡逻兵
}
}
public void notified(StateOfActor state, int pos, GameObject actor)
{
if(state == StateOfActor.ENTER_AREA)
{
//分数加1
}
else
{
//失败
}
}
}
工厂
工厂模式在飞碟中已经有学习过了,这里不多讲,有兴趣就看下飞碟那个博客:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Factory : MonoBehaviour {
private static List<GameObject> usedPatrol = new List<GameObject>();
//正在使用的巡逻兵列表
private static List<GameObject> freePatrol = new List<GameObject>();
//空闲巡逻兵列表
//在指定的位置上放置巡逻兵
public GameObject setOnPos(Vector3 pos, Quaternion direction)
{
if(freePatrol.Count == 0)
{
GameObject aGameObject = Instantiate(Resources.Load("prefabs/Patrol")
, pos, direction) as GameObject;
// 新建实例,将位置设置成为targetposition,将面向方向设置成faceposition
usedPatrol.Add(aGameObject);
Debug.Log(aGameObject);
}
else
{
usedPatrol.Add(freePatrol[0]);
freePatrol.RemoveAt(0);
usedPatrol[usedPatrol.Count - 1].SetActive(true);
usedPatrol[usedPatrol.Count - 1].transform.position = pos;
usedPatrol[usedPatrol.Count - 1].transform.localRotation = direction;
}
return usedPatrol[usedPatrol.Count - 1];
}
public void removeUsed(GameObject obj)
{
obj.SetActive(false);
usedPatrol.Remove(obj);
freePatrol.Add(obj);
}
}
计分系统
这个也不用多讲了,相关的解释在注释中也有标明:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class ScoreRecorder {
public Text scoreText;
//计分的文本
private int score = -1;
//记录分数
//重置分数,到时重置游戏需要
public void resetScore()
{
score = -1;
}
//增加分数
public void addScore(int add_one)
{
score += add_one;
scoreText.text = "Score:" + score;
}
//reset所用
public void setDisActive()
{
scoreText.text = "";
}
public void setActive()
{
scoreText.text = "Score:" + score;
}
}
界面的UI
主要是分数的显示跟游戏失败的提示(比较粗糙):
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class UIcontroller{
public Text centerText;
//失败的UI
public void loseGame()
{
centerText.text = "Lose!";
}
//重置
public void resetGame()
{
centerText.text = "";
}
}
场景的设计
- 地面:Plane
- 墙壁:cube+贴图即可实现
- 触发器:用空对象,加载触发器的组件即可,注意空间的大小
本次实验的大坑:
参考上一届师兄的代码,可能是unity版本的不同,掉了几次大坑
观察者模式
- 一开始先入为主思想,直接把类命名为Publish跟Observer,但是问题就在本身这两个类名是全局变量,在vs中,等你在定义函数时会爆出二义性的错误,而unity的控制台也有相应的报错,只需要改下类型即可
- 触发器一开始以为是一类对象,在网上查找相关信息后,确实其实是空对象加上触发器的组件