游戏设计要求:
- 创建一个地图和若干巡逻兵(使用动画);
- 每个巡逻兵走一个3~5个边的凸多边型,位置数据是相对地址。即每次确定下一个目标位置,用自己当前位置为原点计算;
- 巡逻兵碰撞到障碍物,则会自动选下一个点为目标;
- 巡逻兵在设定范围内感知到玩家,会自动追击玩家;
- 失去玩家目标后,继续巡逻;
- 计分:玩家每次甩掉一个巡逻兵计一分,与巡逻兵碰撞游戏结束;
项目
gif图(图片较大加载较慢),若无法加载,可以到此链接查看
游戏实现
订阅/发布模式的使用
相比与c++,java,js等语言,C#有一个提供订阅/发布模式的语句,分别为delegate和event,一个使用的例子如下:
在巡逻兵的脚本中,可以定义下面的事件,当巡逻兵捕捉到玩家的时候,可以通过CatchHeroEvent()发布消息
public delegate void CatchHero();
public static event CatchHero CatchHeroEvent;
而在接收者中,可以使用一种很简单的方式接收到发布的消息:
PatrolScript.CatchHeroEvent += patrolCatchHero;
巡逻兵的巡逻方法
我们在挂载到巡逻兵的脚本上设计以下方法:当巡逻兵应该去抓捕玩家(shouldChase())且仍然未抓到(!hasCatch)时,把追捕的目标设置成玩家(hero)所在的位置;如果玩家走出区域,会通过EscapeEvent发送一条事件告知gamecontrol可以加分,并获得下一个随机的位置
void Update () {
if (shouldChase() && !hasCatch) {
target = hero.gameobject.transform.position;
isChasing = true;
} else if(isChasing) { // 停止抓捕
isChasing = false;
EscapeEvent();
target = getRandomPosition();
} else if((target - gameObject.transform.position).magnitude < 3) {
target = getRandomPosition();
}
move();
}
巡逻兵的碰撞检测
当巡逻兵碰撞到障碍物,则会自动选下一个点为目标。
void OnCollisionStay(Collision e) {
string name = e.gameObject.name;
if(name.Contains("Hero")) {
CatchHeroEvent();
hasCatch = true;
target = getRandomTarget();
} else if(name.Contains("wall") || name.Contains("Patrol")) {
target = getRandomTarget();
}
}
当捕捉到玩家时,发出一条捕捉到的消息;当碰撞到其他巡逻兵或墙的时候,自动寻找下一个目标
UML图
其他
关于多边形巡逻的实现
事实上,我们在游戏中没有采用让小兵进行多边形巡逻的设计:所有小兵按同样的速率,按相似的多边形巡逻,让游戏的视觉体验不太好。不过为了达到这个目标,这次我们在多设计了两个Action函数:CycleAction和RotateToAction。实现多边形巡逻的方法为:
1. 巡逻兵走到多边形的起点
2. 巡逻兵走到下一个点
3. 转向,面向需要走到的下一个点
4. 循环回到第二步
循环动作
这个动作的设计思路是让游戏对象进行完一组前导动作之后,进入循环的动作中
// 循环动作
public class CycleAction : Action, Callback {
public List<Action> _begin;
public List<Action> _cycle;
private int i = 0;
private bool inCycle = false;
public static Action getAction(List<Action> begin, List<Action> cycle) {
CycleAction action = ScriptableObject.CreateInstance<CycleAction>();
action._begin = begin;
action._cycle = cycle;
return action;
}
public override void Start() {
inCycle = false;
foreach (Action ac in _cycle) {
ac.callback = this;
}
foreach (Action ac in _begin) {
ac.callback = this;
}
if (_begin.Count != 0) {
_begin[0].Start();
} else if (_cycle.Count != 0) {
inCycle = true;
_cycle[0].Start();
} else {
if (callback != null) {
callback.call();
}
destroy = true;
}
}
public override void Update() {
if(inCycle) {
if(_cycle.Count != 0) {
_cycle[i].Update();
} else {
this.destroy = true;
}
} else {
_begin[i].Update();
}
}
public override void OnGUI() {
if (inCycle) {
_cycle[i].OnGUI();
} else {
_begin[i].OnGUI();
}
}
// 子动作的回调函数
public void call() {
if(inCycle) {
i = (i + 1) % _cycle.Count;
_cycle[i].Start();
} else {
_begin[i].destroy = true;
if (++i < _begin.Count) {
// 开始操作还没结束
_begin[i].Start();
} else if (_cycle.Count != 0) {
// 开始操作已结束,进入循环
i = 0;
_cycle[0].Start();
inCycle = true;
} else {
// 循环队列无动作
this.destroy = true;
}
}
}
}
转体动作
这个Action的作用在于让指定对象在给定时间或角速度下转向,面向某个方向
// 转体动作
public class RotateToAction : Action {
private Vector3 targetPos;
private Quaternion dist;
private Vector3 Del;
private float during;
private float time = 0;
public static Action getAction(GameObject gameobject, Vector3 dist, float during) {
RotateToAction ac = ScriptableObject.CreateInstance<RotateToAction>();
ac.targetPos = dist;
ac.gameobject = gameobject;
ac.during = during;
return ac;
}
public static Action getActionByVelocity(GameObject gameobject, Vector3 dist, float velocity) {
Vector3 e = Quaternion.LookRotation(dist).eulerAngles - gameobject.transform.rotation.eulerAngles;
float during = e.magnitude / velocity;
return getAction(gameobject, dist, during);
}
public override void Start() {
time = 0;
dist = Quaternion.LookRotation(targetPos - gameobject.transform.position);
Del = dist.eulerAngles - gameobject.transform.rotation.eulerAngles;
}
public override void Update() {
if (time < during) {
gameobject.gameObject.transform.rotation *= Quaternion.Euler(Del * Time.deltaTime / during);
time += Time.deltaTime;
} else {
gameobject.transform.rotation = dist;
destroy = true;
if (callback != null) {
callback.call();
}
}
}
}