初识发布与订阅模型
完成一下课堂的实验:Garen跳到最高点时发出消息“JumpTopPoint”,发出通知消息,订阅者接收到消息,做出相应反馈。
放个代码:
//发布
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class GarenEvent : MonoBehaviour {
public delegate void GarenSubject(GameObject self, string message); //定义回调函数类型
public static event GarenSubject OnGarenSubjectNotify; //定义subject
void JumpTopPoint()
{
Debug.Log("Jump to the top point!");
if (OnGarenSubjectNotify != null)
OnGarenSubjectNotify(this.gameObject, "AtTop"); //发出通知,事件由谁处理, 如何处理都不需要知道
}
// Use this for initialization
void Start () {
}
// Update is called once per frame
void Update () {
// JumpTopPoint();
}
}
//订阅,处理
public class SceneController : MonoBehaviour {
void ReceiveMessage(Object sender, string info)
{
Debug.Log("Has Received");
}
void OnEnable()
{
GarenEvent.OnGarenSubjectNotify += ReceiveMessage; //订阅
}
void OnDisable()
{
GarenEvent.OnGarenSubjectNotify -= ReceiveMessage; //取消
}
// Use this for initialization
void Start () {
}
// Update is called once per frame
}
把这两个代码文件都挂在Garen上就可以了,结果大概是这个样子:当Garen跳到最高点的时候(这里需要在Animation窗口手动添加Animation Event,在最高点key frame右键添加Event,Function就写JumpTopPoint,如果不手动添加Event,是不会有任何反应的,因为代码里并没有调用JumpTopPoint();),会在控制台显示
本周作业
游戏设计要求:
创建一个地图和若干巡逻兵(使用动画);
每个巡逻兵走一个3~5个边的凸多边型,位置数据是相对地址。即每次确定下一个目标位置,用自己当前位置为原点计算;
巡逻兵碰撞到障碍物,则会自动选下一个点为目标;
巡逻兵在设定范围内感知到玩家,会自动追击玩家;
失去玩家目标后,继续巡逻;
计分:玩家每次甩掉一个巡逻兵计一分,与巡逻兵碰撞游戏结束,玩家失败;玩家得分大于等于5,游戏结束,玩家胜利。
1.游戏素材获取:参考了Unity3D 从入门到放弃 ——巡逻兵 观察者模式的代码。巡逻兵和主角的素材直接用了这位大佬的,围墙(Cube)这些游戏场景的组件也是照着他写的。游戏主场景大概是这个样子的组成
PatrolTriggers是对应墙的通道,预留给Actor进入不同的区域,但是限制巡逻兵只能在自己的区域内进行巡逻。
ActorTriggers是指巡逻兵,当Actor触发某个Trigger,相当于碰撞。
canvas包含centerText和scoreText两个子对象,分别用来显示游戏失败(成功)和当前分数。
2.工厂模式产生巡逻兵
类似打飞碟游戏中的DiskFactory,维护一个“空闲”链表和一个“正在使用”链表,当“空闲”链表结点数小于等于0时意味着需要生产新的巡逻兵,即需要实例化Patrol的预制;否则,就在“空闲”链表中摘一个空闲结点下来使用即可。
//因为动画人物是有朝向的,所以实例化的时候需要指定
if (inFree.Count <= .0)
{
GameObject newPatrol = Instantiate(Resources.Load("prefabs/Patrol"), targetposition, faceposition) as GameObject;
inUsed.Add(newPatrol);
}
else
{
inUsed.Add(inFree[0]);
inFree.RemoveAt(0);
inUsed[inUsed.Count - 1].SetActive(true);
inUsed[inUsed.Count - 1].transform.position = targetposition;
inUsed[inUsed.Count - 1].transform.localRotation = faceposition;
}
3.Animator
当Actor是ALIVE时,它的动作就是在idle(空闲站着)和run之间切换,当Actor的currentState为DEATH时,会直接切换成die,结束一切动作。
Patrol没有死亡的状态,他的运动在巡逻(walk)->追踪(run)->站立(idle,游戏结束时一切动作停止)切换。
这两个Animator直接挂在对应的预制上就可以了。
4.发布者订阅者
当Actor触发了某些【游戏规则】,就可以发布相应的信息,然后通过信息的参数,订阅者就会做出相应的动作。比如,当Actor进入到某个巡逻兵的区域,他就会发布信息publish.notify(ActorState.BE_FOLLOWED, patrol, this.gameObject);通知对应区域的巡逻兵,巡逻兵收到消息,就可以开始追踪主角。当主角离开某个区域,此时会触发另一个通道的trigger,所以前一个区域的巡逻兵放弃追踪,现在这个区域的巡逻兵继续进行追踪。当Actor和某个巡逻兵发生碰撞,也会发布相应的消息 publish.notify(ActorState.DEATH, 0, null);通知关注Actor是否存活的类进行对应操作。
除了巡逻兵是订阅者,SceneController也是订阅者,因为需要处理加分的事项。
//Patrol.cs
public void notified(ActorState state, int pos, GameObject actor)
{
if (state == ActorState.BE_FOLLOWED)
{
if (pos == this.gameObject.name[this.gameObject.name.Length - 1] - '0')
FollowAction(actor);
else
PatrolAction();
}
// 角色死亡,结束动作
else if (state == ActorState.DEATH)
{
stopAllAction();
}
}
//SceneController.cs
public void notified(ActorState state, int pos, GameObject actor)
{
if (state == ActorState.BE_FOLLOWED)
record.addScore(1); //摆脱巡逻兵,加分
else
{
UI.loseGame();
}
if (record.score >= 5) //玩家得分超过5,获得游戏胜利
{
UI.winGame();
state = ActorState.DEATH; //直接利用Death结束游戏
CancelInvoke();
}
}
5.wsad控制主角移动
void FixedUpdate()
{
if (!ani.GetBool("isLive"))
return;
float x = Input.GetAxis("Horizontal");
float z = Input.GetAxis("Vertical");
velocity = new Vector3(x, 0, z);
ani.speed = 1 + ani.GetFloat("Speed") / 2;
ani.SetFloat("Speed", Mathf.Max(Mathf.Abs(x), Mathf.Abs(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;
}
6.巡逻兵的移动
定义巡逻兵的几种运动方式:
public enum ActionState : int { IDLE, WALKLEFT, WALKFORWARD, WALKRIGHT, WALKBACK }
通过枚举常量来调用不同的运动方法,在游戏一开始时巡逻兵就可以按照向左->向前->向右->向后的运动轨迹走出一个多边形。
//循环状态
if ((int)currentState > 4)
currentState -= 4;
// 改变当前状态
else 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;
}
为了实现“巡逻兵碰撞到障碍物,则会自动选下一个点为目标”,实现时我选择碰撞后走相反方向【其实这样有一点小问题后面说bug的时候再说明】
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;
}
需要注意的是,当切换动画动作时,要销毁对象之前的动作,保证任意时刻对象都只能有一个动作在进行。
7.人机交互部分
还是和之前用的模式差不多,在UserGUI生成一个Start按钮,按下按钮会执行StartGame()的函数,StartGame()中调用LoadResources(),LoadResources中直接实例化Actor,利用循环调用工厂模式中getPatrolObject()函数产生四个巡逻兵,游戏开始。导演场记协同,其他的就不说了。
if (GUI.Button(new Rect(castw(2f) + 20, casth(6f) + 60, 100, 100), "Start"))
{
action.StartGame();
}
public void StartGame()
{
this.isStarted = true;
this.isPaused = false;
LoadResources();
}
private void LoadResources()
{
Instantiate(Resources.Load("prefabs/Ami"), new Vector3(0, 0, 0), Quaternion.Euler(new Vector3(0, 180, 0)));
for (int i = 0; i < 4; i++)
{
//利用factory产生巡逻兵
GameObject patrol = factory.getPatrolObject(new Vector3(posx[i], 0, posz[i]), Quaternion.Euler(new Vector3(0, 180, 0)));
patrol.name = "Patrol" + (i + 1);
}
}
8.Action和ActionManager
定义了三个Action类,分别是IdleAction、WalkAction和RunAction。各个类的具体实现差不多,走和跑的区别就是速度,idle实际什么都不用做。ActionManager和SSActionManager差不多,需要注意的是写的是FixedUpdate而不是Update()
遇到的bug及困难:
1.做Garen的时候无意间把publish脚本挂在了两个对象上,一个是Garen,一个是他的父对象Empty,因此出现了Error,报错信息大概是AnimationEvent ‘JumpTopPoint’ has no receiver! Are you missing a component?不是第一次因为挂错脚本而产生无法解决的bug了,以后一定要注意……
2.巡逻兵巡逻的时候,编写代码遇到障碍会按反方向走,但是某些时刻就会出现一个很诡异的角度,刚好夹在墙边,于是正反方向行走,就会出现在墙角“迷失”的情况,这个没有想到怎么解决。
3.关于Update和FixedUpdate,原来没有很注意在ActionManager里面的区别,因此按照以前直接写了FixedUpdate(ac.FixedUpdate();)本来觉得理所应当因为Actor和Patrol都是刚体,在对应cs代码中写的是也是FixedUpdate,然后在Patrol.cs中update直接调用父类(SSActionManager)的Update。本来以为这样就可以正常运行了,但是发现巡逻兵并不能够走动,然后SSAction报错,throw new System.NotImplementedException(“Physics Action Start Error!”);仔细检查了一下发现SSActionManager中的Update应该就是Update,额外对应的ActionsManager才应该是ac.FixedUpdate();