【Unity3D】智能巡逻兵

游戏规则与要求


游戏设计要求:
  • 创建一个地图和若干巡逻兵(使用动画);
  • 每个巡逻兵走一个3~5个边的凸多边型,位置数据是相对地址。即每次确定下一个目标位置,用自己当前位置为原点计算;
  • 巡逻兵碰撞到障碍物,则会自动选下一个点为目标;
  • 巡逻兵在设定范围内感知到玩家,会自动追击玩家;
  • 失去玩家目标后,继续巡逻;
  • 计分:玩家每次甩掉一个巡逻兵计一分,与巡逻兵碰撞游戏结束;
程序设计要求:
  • 必须使用订阅与发布模式传消息

  • 工厂模式生产巡逻兵

游戏规则
  • 玩家通过键盘控制人物在地图上移动
  • 地图分为九个部分,有八个巡逻兵在出生点以外的部分巡逻
  • 当玩家进入一个部分时该部分的巡逻兵就会追逐玩家,若玩家甩开巡逻兵则分数加一,若玩家与巡逻兵触碰则游戏结束。

具体实现


人物模型

使用了前辈的资源 YBoy

在这里插入图片描述

  • 动画状态机

将所使用的动画都拖入人物动画器的Base Layer中,实现人物动作:

  • 按WASD进行行走和转向
  • 站立不动时按下空格后跳一小步,行走时按下空格向前翻滚
  • 奔跑时按下空格向前跳跃,在空中跳跃完成后下落,下落到地面时翻滚
  • 按SHIFT奔跑

在这里插入图片描述

  • ground动画混合树

由站立,走路,奔跑三个动画组成

在这里插入图片描述

  • 创建过渡与参数控制

  • ActorController

脚本控制参数

//变量
public GameObject model;    		//人物模型
public PlayerInput pi;				//用户输入
public float walkSpeed = 1.5f;		//行走速度
public float runMultiplier = 2.7f;	//奔跑速度
public float jumpVelocity = 4f;		//跳跃速度
public float rollVelocity = 1f;		//翻滚速度

[SerializeField]
private Animator anim;				//动画控制器
private Rigidbody rigid;			//刚体组件
private Vector3 planarVec; 			//平面移动向量
private Vector3 thrustVec;			//跳跃冲量

private bool lockPlanar = false;    //跳跃时锁死平面移动向量

//刷新每秒60次
void Update() {
   //修改动画混合树
   float targetRunMulti = pi.run ? 2.0f : 1.0f;
   anim.SetFloat("forward", pi.Dmag * Mathf.Lerp(anim.GetFloat("forward"), targetRunMulti, 0.3f));
   //播放翻滚动画
   if (rigid.velocity.magnitude > 1.0f) {
       anim.SetTrigger("roll");
   }
   //播放跳跃动画
   if (pi.jump) {
       anim.SetTrigger("jump");
   }
   
   //转向
   if(pi.Dmag > 0.01f) {
       Vector3 targetForward = Vector3.Slerp(model.transform.forward, pi.Dvec, 0.2f);
       model.transform.forward = targetForward;
   }
   if(!lockPlanar) {
       //保存供物理引擎使用
       planarVec = pi.Dmag * model.transform.forward * walkSpeed * (pi.run ? runMultiplier : 1.0f);
   }
   
}

//物理引擎每秒50次
private void FixedUpdate() {
     //修改位置、速度
     rigid.velocity = new Vector3(planarVec.x, rigid.velocity.y, planarVec.z) + thrustVec;
     //一帧
     thrustVec = Vector3.zero;
 }

  • PlayerInput

获取玩家输入,控制角色的行走与转向,同时,在空中不允许用户输入,还需要解决同时按下W和A时移动速度会与只按下W不同的问题,即将矩形坐标转为圆坐标。

public class PlayerInput : MonoBehaviour {
    [Header("---- KeyCode Settings ----")]
    public string keyUp = "w";
    public string keyDown = "s";
    public string keyLeft = "a";
    public string keyRight = "d";

    public string keyA = "left shift";
    public string keyB = "space";
    public string keyC = "k";
    public string keyD;

    public string keyJUp = "up";
    public string keyJDown = "down";
    public string keyJLeft = "left";
    public string keyJRight = "right";

    [Header("---- Output Settings ----")]
    public float Dup;
    public float Dright;
    public float Dmag;
    public Vector3 Dvec;

    public float Jup;
    public float Jright;

    public bool run;
    public bool jump;
    private bool lastJump;

    [Header("---- Other Settings ----")]
    public bool inputEnabled = true;

    private float targetDup;
    private float targetDright;
    private float velocityDup;
    private float velocityDright;

    void Start() {}

    void Update() {
        Jup = (Input.GetKey(keyJUp)) ? 1.0f : 0 - (Input.GetKey(keyJDown) ? 1.0f : 0);
        Jright = (Input.GetKey(keyJRight)) ? 1.0f : 0 - (Input.GetKey(keyJLeft) ? 1.0f : 0);

        targetDup = (Input.GetKey(keyUp) ? 1.0f : 0) - (Input.GetKey(keyDown) ? 1.0f : 0);
        targetDright = (Input.GetKey(keyRight) ? 1.0f : 0) - (Input.GetKey(keyLeft) ? 1.0f : 0);

        if(!inputEnabled) {
            targetDup = 0;
            targetDright = 0;
        }
        //平滑变动
        Dup = Mathf.SmoothDamp(Dup, targetDup, ref velocityDup, 0.1f);
        Dright = Mathf.SmoothDamp(Dright, targetDright, ref velocityDright, 0.1f);

        /*矩形坐标转圆坐标*/
        Vector2 tempDAxis = SquareToCircle(new Vector2(Dup, Dright));
        float Dup2 = tempDAxis.x;
        float Dright2 = tempDAxis.y;

        Dmag = Mathf.Sqrt((Dup2 * Dup2) + (Dright2 * Dright2));
        Dvec = Dright * transform.right + Dup * transform.forward;
        run = Input.GetKey(keyA);

        /*跳跃*/
        bool newJump = Input.GetKey(keyB);
        lastJump = jump;
        if(lastJump == false && newJump == true) {
            jump = true;
        }
        else {
            jump = false;
        }
    }

    /*矩形坐标转圆坐标*/
    private Vector2 SquareToCircle(Vector2 input) {
        Vector2 output = Vector2.zero;
        output.x = input.x * Mathf.Sqrt(1 - (input.y * input.y) / 2.0f);
        output.y = input.y * Mathf.Sqrt(1 - (input.x * input.x) / 2.0f);
        return output;
    }
}

  • FSM

用于发送消息

  1. FSMOnEnter——进入状态时向父级发送消息
  2. FSMOnExit——在状态退出时发出消息
  3. FSMOnUpdate——在状态刷新时发出消息,用来实现翻滚和跳跃
  4. FSMClearSignals——在状态进入和退出时清除多余的Trigger,否则按一下空格会给jump参数储存多个Trigger,跳跃多次
    public class FSMClearSignals : StateMachineBehaviour {
    public string[] ClearAtEnter;
    public string[] ClearAtExit;
    
    public override void OnStateEnter(Animator animator, AnimatorStateInfo stateInfo, int layerIndex) {
        foreach (var signal in ClearAtEnter) {
            animator.ResetTrigger(signal);
        }
    }
    
    public override void OnStateExit(Animator animator, AnimatorStateInfo stateInfo, int layerIndex) {
        foreach (var signal in ClearAtExit) {
            animator.ResetTrigger(signal);
        }
    }
    

}
```

巡逻兵

同样是 YBoy的
在这里插入图片描述

与玩家使用同一个模型和动画器,不过通过代码控制其只有行走和奔跑两个动作,巡逻时行走,追逐玩家时奔跑。

控制其的代码实现:

  • GuardPatrolAction

实现追逐玩家部分,其中Update只更新平面移动的向量,FixedUpdate使用该向量进行移动,通过修改rigid的velocity来s实现移动。

//Update只更新平面移动的向量
    public override void Update() {
        //保留供物理引擎调用
        planarVec = gameobject.transform.forward * data.walkSpeed;
    }
//FixedUpdate使用该向量进行移动,通过修改rigid的velocity来移动
    public override void FixedUpdate() {
        //巡逻
        Gopatrol();
        //玩家进入该区域,巡逻结束,开始追逐
        if (data.playerSign == data.sign) {
            this.destroy = true;
            this.callback.SSActionEvent(this, SSActionEventType.Competeted, 0, this.gameobject);
        }
    }

  • GuardFollowAction

实现玩家离开后正常巡逻,和与GuardPatrolAction类似。

    public override void Update() {
        //保留供物理引擎调用
        planarVec = gameobject.transform.forward * speed;
    }

    public override void FixedUpdate() {
        transform.LookAt(player.transform.position);
        rigid.velocity = new Vector3(planarVec.x, rigid.velocity.y, planarVec.z);
        
        //如果玩家脱离该区域则继续巡逻
        if (data.playerSign != data.sign) {
            this.destroy = true;
            this.callback.SSActionEvent(this, SSActionEventType.Competeted, 1, this.gameobject);
        }
    }

  • GuardActionManager

实现巡逻兵动作切换的功能,回调函数SSActionEvent,当一个动作被销毁时调用另一个动作


    public void SSActionEvent(
        SSAction source, SSActionEventType events = SSActionEventType.Competeted,
        int intParam = 0, GameObject objectParam = null) {
        if (intParam == 0) {
            //追逐
            GuardFollowAction follow = GuardFollowAction.GetSSAction(player);
            this.RunAction(objectParam, follow, this);
        } else {
            //巡逻
            GuardPatrolAction move = GuardPatrolAction.GetSSAction(objectParam.gameObject.GetComponent<GuardData>().start_position);
            this.RunAction(objectParam, move, this);
            Singleton<GameEventManager>.Instance.PlayerEscape();
        }
    }

地图部分

地图分为九个格子,每个区域有自己的碰撞器,并挂载AreaCollide.cs

在这里插入图片描述
AreaCollide:当玩家进入时设置场景控制器中的玩家区域标志,然后场景控制器通知对应的巡逻兵追逐玩家。


public class AreaCollide : MonoBehaviour {
    public int sign = 0;
    private FirstSceneController sceneController;

    private void Start() {
        sceneController = SSDirector.GetInstance().CurrentScenceController as FirstSceneController;
    }

    void OnTriggerEnter(Collider collider) {
        if (collider.gameObject.tag == "Player") {
            sceneController.playerSign = sign;
        }
    }
}

消息订阅/发布模式

也称为观察者(Observer)模式:多个对象间存在一对多关系,当一个对象发生改变时,把这种改变通知给其他多个对象,从而影响其他对象的行为。

  • GameEventManager

专门发布事件,订阅者可以订阅该类的事件,当其他类发生改变的时候,会使用GameEventManager的方法发布消息,触发相应事件。

    public delegate void ScoreEvent();
    public static event ScoreEvent ScoreChange;
    
    public delegate void GameoverEvent();
    public static event GameoverEvent GameoverChange;

    public void PlayerEscape() {
        if (ScoreChange != null) {
            ScoreChange();
        }
    }

    public void PlayerGameover(){
        if (GameoverChange != null) {
            GameoverChange();
        }
    }

  • FirstSenceController

作为订阅者,订阅了GameEventManager中的事件,只要相应事件发生,就会导致场景控制器调用注册的方法。

void OnEnable() {
    GameEventManager.ScoreChange += AddScore;
    GameEventManager.GameoverChange += Gameover;
}
void OnDisable() {
    GameEventManager.ScoreChange -= AddScore;
    GameEventManager.GameoverChange -= Gameover;
}

void AddScore() {
    recorder.AddScore();
}

void Gameover() {
    game_over = true;
}

工厂模式

这部分的代码和上一次的作业是基本一致的,就不赘述了

  • GuardFactory

巡逻兵工厂

    private GameObject guard = null;                               //巡逻兵
    private List<GameObject> used = new List<GameObject>();        //正在使用的巡逻兵列表
    private Vector3[] vec = new Vector3[9];                        //每个巡逻兵的初始位置

    public List<GameObject> GetPatrols() {
        int[] pos_x = { -6, 4, 13 };
        int[] pos_z = { -4, 6, -13 };
        int index = 0;
        for(int i=0;i < 3;i++) {
            for(int j=0;j < 3;j++) {
                vec[index] = new Vector3(pos_x[i], 0, pos_z[j]);
                index++;
            }
        }
        for(int i = 0; i < 8; i++) {
            guard = Instantiate(Resources.Load<GameObject>("Prefabs/Guard"));
            guard.transform.position = vec[i];
            guard.GetComponent<GuardData>().sign = i + 1;
            guard.GetComponent<GuardData>().start_position = vec[i];
            guard.GetComponent<Animator>().SetFloat("forward", 1);
            used.Add(guard);
        }   
        return used;
    }

第三人称

在cameraHandle中增加一个空对象cameraPos位置调整到人物模型的脖子后方,并挂载CameraController.cs脚本,用于控制摄像机的位置。

    public PlayerInput pi;
    public float horizontalSpeed = 100f;
    public float verticalSpeed = 80f;
    public float cameraDampValue = 0.5f;

    private GameObject playerHandle;
    private GameObject cameraHandle;
    private float tempEulerX;
    private GameObject model;
    private GameObject camera;

    private Vector3 cameraDampVelocity;
    

    void Awake() {
        cameraHandle = transform.parent.gameObject;
        playerHandle = cameraHandle.transform.parent.gameObject;
        model = playerHandle.GetComponent<ActorController>().model;
        camera = Camera.main.gameObject;
        tempEulerX = 20f;
    }

    // Update is called once per frame
    void FixedUpdate() {
        Vector3 tempModelEuler = model.transform.eulerAngles;
        playerHandle.transform.Rotate(Vector3.up, pi.Jright * horizontalSpeed * Time.fixedDeltaTime);
        tempEulerX -= pi.Jup * verticalSpeed * Time.fixedDeltaTime;
        tempEulerX = Mathf.Clamp(tempEulerX, -35, 30);
        cameraHandle.transform.localEulerAngles = new Vector3(tempEulerX, 0, 0);
        model.transform.eulerAngles = tempModelEuler;

        camera.transform.position = Vector3.SmoothDamp(
            camera.transform.position, transform.position, 
            ref cameraDampVelocity, cameraDampValue);
        camera.transform.eulerAngles = transform.eulerAngles;
    }

其他部分

这部分的代码也都基本沿用之前的

  • UserGUI

实现显示部分

    private IUserAction action;
    private GUIStyle score_style = new GUIStyle();
    private GUIStyle text_style = new GUIStyle();
    private GUIStyle over_style = new GUIStyle();
    void Start () {
        action = SSDirector.GetInstance().CurrentScenceController as IUserAction;
        text_style.normal.textColor = new Color(0, 0, 0, 1);
        text_style.fontSize = 16;
        score_style.normal.textColor = new Color(1,0.92f,0.016f,1);
        score_style.fontSize = 16;
        over_style.fontSize = 25;
    }

    private void OnGUI() {
        GUI.Label(new Rect(Screen.width - 170, 10, 200, 50), "分数:", text_style);
        GUI.Label(new Rect(Screen.width - 130, 10, 200, 50), action.GetScore().ToString(), score_style);
       // GUI.Label(new Rect(Screen.width / 2 - 80, 10, 100, 100), "WASD移动,方向键移动视角", text_style);
        //GUI.Label(new Rect(Screen.width / 2 - 80, 30, 100, 100), "空格跳跃,Shift奔跑", text_style);
       // GUI.Label(new Rect(Screen.width / 2 - 80, 50, 100, 100), "成功躲避巡逻兵追捕加1分", text_style);
        if (action.GetGameover()) {
            GUI.Label(new Rect(Screen.width / 2 - 50, Screen.width / 2 - 250, 100, 100), "游戏结束", over_style);
            if (GUI.Button(new Rect(Screen.width / 2 - 50, Screen.width / 2 - 150, 100, 50), "重新开始")) {
                action.Restart();
                return;
            }
        }  
    }

游戏结果


在这里插入图片描述

后记


熟悉了上课讲的动画和模型,发布与订阅模式定义了一种一对多的依赖关系,实现了让多个订阅者对象同时监听某一个主题对象,这个对象在状态发生变化时会通知所有订阅者对象,使它们能够自动更新自己。

这里就是有点问题,老师的课件中观察者模式和订阅/发布模式是一样的,但搜了一下,有一定区别,如下:

从表面上看:

  • 观察者模式里,只有两个角色 —— 观察者 + 被观察者
  • 发布订阅模式里,却不仅仅只有发布者和订阅者两个角色,还有一个经常被我们忽略的 —— 经纪人Broker

往更深层次讲:

  • 观察者和被观察者,是松耦合的关系
  • 发布者和订阅者,则完全不存在耦合

从使用层面上讲:

  • 观察者模式,多用于单个应用内部
  • 发布订阅模式,则更多的是一种跨应用的模式(cross-application pattern),比如我们常用的消息中间件

感觉还是有一些区别的,之后再仔细看看。

参考博客1
参考博客2

代码地址

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值