Unity3d——巡逻兵游戏(订阅与发布模式)

初识发布与订阅模型
完成一下课堂的实验: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();

猜你喜欢

转载自blog.csdn.net/qq_32335095/article/details/80229956