3d学习笔记(六)——巡逻兵游戏

作业与练习

游戏设计要求

  • 创建一个地图和若干巡逻兵(使用动画);
  • 每个巡逻兵走一个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的控制台也有相应的报错,只需要改下类型即可
  • 触发器一开始以为是一类对象,在网上查找相关信息后,确实其实是空对象加上触发器的组件

猜你喜欢

转载自blog.csdn.net/qq_36312878/article/details/80286185