3D游戏编程学习笔记(七):模型与动画

一、前言

本次3D游戏编程我们将设计一个智能巡逻兵游戏。


二、游戏基本内容及规定

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

三、人物模型与动画制作

人物模型采用了官方的一个第三人称控制资源包Starter Assets,下载导入即可。

由于该资源包自带人物动画,因此我们只需要Assets->Create->Animator Controller创建一个新的动画状态机,然后将走、跑、站立三个状态的动画拖进去设置好状态即可。
请添加图片描述
此外本次游戏还涉及到了墙、地板等其他预制件的制作,在这里就不再赘述,项目地址已留在文章末尾,感兴趣的读者可以在下载下来看看。


三、游戏实现

本次游戏实现运用到了之前所学的诸多编程理念,诸如MVC模式、工厂模式、单例模式、消息订阅/发布模式等等,具体内容可以参考之前的作业。
由于篇幅限制,之前作业中分析过的代码就不再做分析了,我们主要关注新增加的以及发生变化的代码。

  • SceneController
    SceneController负责加载资源,比如房间、玩家、巡逻兵等,以及调度调用巡逻兵的动作管理器使其开始走动。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;

public class SceneController : MonoBehaviour, IUserAction, ISceneController{

    public GuardFactory guardFactory;  //巡逻兵工厂
    public ScoreRecorder recorder;  //记分员
    public GuardActionManager actionManager;    //运动管理器
    public List<GameObject> cells;  //每个小房间
    public GameObject player;   //玩家
    public UserGUI gui;     //交互界面

    public PlayerEventPublisher playerEventPublisher;   //事件发布者

    private List<GameObject> guards;    //场景中所有的巡逻兵
    private bool game_over = false;     //游戏结束

    private void Start() {
        //初始化这个脚本的相关控件
        SSDirector director = SSDirector.GetInstance();
        director.CurrentScenceController = this;
        guardFactory = gameObject.AddComponent<GuardFactory>() as GuardFactory;
        recorder = gameObject.AddComponent<ScoreRecorder>() as ScoreRecorder;
        actionManager = gameObject.AddComponent<GuardActionManager>() as GuardActionManager;
        gui = gameObject.AddComponent<UserGUI>() as UserGUI;

        LoadResources();
    }

    /*ISceneController接口相关函数*/
    public void LoadResources(){
        //加载房间
        for(int i=0;i<9;i++){
            GameObject cell = Instantiate(Resources.Load<GameObject>("Prefabs/Cell"));
            int a = i % 3;
            int b = i / 3;
            cell.transform.position = new Vector3(12 * a, 0, 12 * b);
            cells.Add(cell);
        }
        
        //加载玩家
        player = Instantiate(Resources.Load("Prefabs/Player"), new Vector3(24, 0, 24), Quaternion.identity) as GameObject;
        playerEventPublisher = player.GetComponent<PlayerEventPublisher>();     

        //加载巡逻兵
        guards = guardFactory.GetGuards();

        //让每个巡逻兵都订阅玩家的事件发布者
        //并让每个巡逻兵调用动作管理器开始走动
        for (int i = 0; i < guards.Count; i++) {
            IEventHandler e = guards[i].GetComponent<PatrolData>() as IEventHandler;
            playerEventPublisher.AddListener(e);
            actionManager.GuardWalk(guards[i], player);
        }
    }

    /*IUserAction接口相关函数*/
    public int GetScore() {
        return recorder.GetScore();
    }

    public bool isOver() {
        return game_over;
    }

    public void Restart() {
        //重新生成这个场景即可
        SceneManager.LoadScene("Scenes/SampleScene");
    }

    /*动作管理器会回调的函数*/
    public void Record() {
        recorder.Record();
    }

    /*玩家管理器会回调的函数*/
    public void Gameover() {
        game_over = true;
    }
}
  • PlayerController
    PlayerController负责:
    ①控制玩家的动画设置,通过检测方向键以及shift的变化使得玩家对象进行行走或奔跑动作;
    ②综合第三人称视角方向以及方向键来设置角色移动方向;
    ③根据鼠标移动更新相机的位置
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class PlayerController : MonoBehaviour
{
    private Animator animator;  //动画组件
    private new Rigidbody rigidbody;    //刚体组件
    private new Camera camera;  //视角跟随相机组件
    private Vector3 offset;     //相机偏移量

    public float walk_speed = 3.0f; //行走速度
    public float run_speed = 6.0f;  //跑步速度
    private float speed;    //当前速度
    private float rotate_speed = 3.0f;//旋转速度
    private float camera_rotate;    //当前旋转速度
    public bool isLive = true;      //游戏是否进行中

    void Start()
    {
        //获取主相机
        camera = Camera.main;

        //设置相机的偏移
        offset = this.transform.position;
        camera.transform.position = offset + new Vector3(0, 2, -4);

        //需要刚体组件来协助移动
        rigidbody = GetComponent<Rigidbody>();

        //初始状态为行走
        speed = walk_speed;     

        //获取动画组件
        animator = GetComponent<Animator>();
    }

    private void FixedUpdate() {
        //获取方向键输入
        Vector3 vel = rigidbody.velocity;
        float v = Input.GetAxis("Vertical");
        float h = Input.GetAxis("Horizontal");
        bool isMove = (Mathf.Abs(v) > 0.05 || Mathf.Abs(h) > 0.05);//防止误触

        //判断游戏是否进行,来确定是否可以采用走路、跑步动画
        animator.SetBool("isLive", isLive);

        //如果有方向键变化,则设置动画为走
        if(isMove){
            animator.SetBool("isWalk", true);
        }
        if(!isMove){
            animator.SetBool("isWalk", false);
        }

        //Shift切换走跑动作
        if(Input.GetKeyDown("left shift")){
            bool ret = animator.GetBool("isRun");
            animator.SetBool("isRun", !ret);
            speed = run_speed;
        }

        //在游戏进行状态下,控制角色移动
        if(isMove && isLive){
            //获得相机的正对方向
            camera_rotate = camera.transform.eulerAngles.y / 180 * Mathf.PI;

            //给刚体一个速度,让角色向镜头方向,并综合方向键进行移动
            float sr = Mathf.Sin(camera_rotate);
            float cr = Mathf.Cos(camera_rotate);
            rigidbody.velocity = new Vector3((v * sr + h * cr) * speed, 0, (v * cr - h * sr) * speed);

            //角色也要面向镜头前方的位置
            transform.rotation = Quaternion.LookRotation(new Vector3((v * sr + h * cr), 0, (v * cr - h * sr)));
        }
        else{
            rigidbody.velocity = Vector3.zero;
        }
    }

    /*采用LateUpdate来加载相机的相关属性*/
    private void LateUpdate() {
        //更新相机的位置,跟随
        camera.transform.position += this.transform.position - offset;
        offset = this.transform.position;

        //鼠标控制相机水平移动
        float mouseX = Input.GetAxis("Mouse X") * rotate_speed;
        camera.transform.RotateAround(this.transform.position, Vector3.up, mouseX);
    }
}

  • SSEventPublishier和PlayerPublishier
    SSEventPublishier负责:
    ①通知所有订阅者;
    ②在SSEventPublishier的子类PlayerPublishier每一次进行Update时,进行检测角色是否出界以及目前所在的房间位置
public class SSEventPublisher : MonoBehaviour {
    private List<IEventHandler> listeners;

    public SSEventPublisher(){
        listeners = new List<IEventHandler>();
    }

    //通知所有的订阅者
    public void Notify(int e){
        for(int i=0; i<listeners.Count; i++){
            IEventHandler eventHandler = listeners[i];
            eventHandler.Reaction(e);
        }
    }

    public void AddListener(IEventHandler e){
        listeners.Add(e);
    }

    public void RemoveListner(IEventHandler e){
        listeners.Remove(e);
    }
}

public class PlayerEventPublisher : SSEventPublisher{
    private int player_sign;    //当前玩家所处的房间
    public SceneController sceneController;

    private void Start() {
        sceneController = SSDirector.GetInstance().CurrentScenceController as SceneController;
    }
    
    private void Update() {
        //获得当前所在房间号
        int ret = FindPosition(this.transform.position);

        //发生变化
        if(ret != player_sign){
            if(ret >= 0 && ret <= 8){
                //玩家还在9个房间中,通知所有巡逻兵,让他们的GuardData变化,从而采取不同操作
                player_sign = ret;
                Notify(player_sign);
            }
            else{
                //玩家出界
                GameOver();
            }
            
        }
    }

    private int FindPosition(Vector3 pos){
        int a = Mathf.FloorToInt((pos.x + 6) / 12); //以中心定位,因此需要加上偏移
        int b = Mathf.FloorToInt((pos.z + 6) / 12);
        if(a >= 0 && a <= 2 && b >= 0 && b <= 2){
            return 3 * a + b;
        }
        else{
            return -1;
        }
    }

    private void OnCollisionEnter(Collision other) {
        if(other.gameObject.name.Contains("Guard")){//撞到巡逻兵,游戏结束
            GameOver();
        }
    }

    private void GameOver(){
        //通知三方做处理:
        //玩家控制器灭活,取消玩家的动画和移动
        PlayerController playerController = GetComponent<PlayerController>();
        playerController.isLive = false;

        //场景总控制器
        sceneController.Gameover();

        //巡逻兵的GuardData变化,取消巡逻兵的动作(立刻回调)并取消动画
        Notify(-1);
    }
}
  • PatrolData
    PatrolData负责提供巡逻兵所需的数据以及一些事件处理,例如结束灭活等。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class PatrolData : MonoBehaviour, IEventHandler
{
    public float walk_speed = 0.5f;        //移速
    public float run_speed = 0.5f;
    public int sign;                      //巡逻兵所在区域
    public bool isRun = false;            //当前是否在追逐玩家
    public bool isLive = true;            //当前是否活跃
    public Vector3 start_position;        //巡逻兵初始位置   

    /*IEventHandler响应函数*/
    public void Reaction(int pos){
        //游戏结束,灭活,取消巡逻兵的动作(立刻回调)并取消动画
        if(pos == -1){
            isLive = false;
            return;
        }

        //若玩家处于巡逻兵一样的位置,开始追逐;否则,结束追逐
        isRun = (pos == sign);
    }
}

  • PatrolFactory
    PatrolFactory用于生成巡逻兵,因此有一个队列放置创建好的巡逻兵object并且用作返回,具体生成逻辑见代码注释。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

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

    public List<GameObject> GetGuards() {
        //为每一个巡逻兵,在一定范围内随机生成位置。
        int[] pos_x = {-5, 7, 19};
        int[] pos_z = {-5, 7, 19};
        int index = 0;
        for(int i=0;i < 3;i++) {
            for(int j=0;j < 3;j++) {
                pos_x[i] += Random.Range(0,3);
                pos_z[i] += Random.Range(0,3);
                vec[index] = new Vector3(pos_x[i], 0, pos_z[j]);
                index++;
            }
        }

        //加载巡逻兵,初始化位置和sign,并放在数组中。
        for(int i = 0; i < 8; i++) {
            guard = Instantiate(Resources.Load<GameObject>("Prefabs/Guard"));
            guard.transform.position = vec[i];
            guard.GetComponent<PatrolData>().sign = i;
            guard.GetComponent<PatrolData>().start_position = vec[i];
            used.Add(guard);
        }   
        return used;
    }
}

  • PatrolWalkAction
    PatrolWalkAction负责巡逻兵行走动作的管理,具体包括:①初始化的时候, GuardPatrolAction加载参数, 并且让巡逻兵播放行走的动画;②随机生成一个边长数值,令巡逻兵沿着某个方向走随机距离
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class GuardWalkAction : SSAction
{         
    private float pos_x, pos_z;     //移动位置记录
    private float move_length;      //移动的边长
    private bool turn_sign = true;  //是否转向        
    private int dirction = 0;       //当前方向

    private PatrolData data;         //巡逻兵数据
    private Animator anim;          //巡逻兵动画
    private Rigidbody rigid;        //巡逻兵刚体

    public override void Start()
    {
        //加载组件
        data = gameobject.GetComponent<PatrolData>();
        anim = gameobject.GetComponent<Animator>();
        rigid = gameobject.GetComponent<Rigidbody>();

        //设置走动动画
        anim.SetBool("isLive", true);
        anim.SetBool("isWalk", true);
        anim.SetBool("isRun", false);
    }

    public static GuardWalkAction GetSSAction(Vector3 location) {
        //确定初始位置
        GuardWalkAction action = CreateInstance<GuardWalkAction>();
        action.pos_x = location.x;
        action.pos_z = location.z;

        //设定移动矩形的边长
        action.move_length = Random.Range(6, 10);
        return action;
    }

    public override void FixedUpdate()
    {
        //观察GuardData中的变量isLive(Reaction会改变这个变量)
        //若被灭活,则取消巡逻兵的动作(立刻回调)并取消动画
        if(!data.isLive){
            anim.SetBool("isLive", false);
            this.destroy = true;
            this.callback.SSActionEvent(this, SSActionEventType.Competeted, -1, this.gameobject);
            return;
        }

        //若不被灭活,则按规则走动
        Walk();

        //观察GuardData中的变量isRun(Reaction会改变这个变量)
        //如果玩家进入该区域则开始追击
        if(data.isRun){
            this.destroy = true;
            this.callback.SSActionEvent(this, SSActionEventType.Competeted, 0, this.gameobject);
        }
    }

    private void Walk(){
        if (turn_sign) {
            //按照矩形逆时针移动
            switch (dirction) {
                case 0:
                    pos_x += move_length;
                    break;
                case 1:
                    pos_z += move_length;
                    break;
                case 2:
                    pos_x -= move_length;
                    break;
                case 3:
                    pos_z -= move_length;
                    break;
            }
            turn_sign = false;
        }

        //获取目标位置,并转向目标位置
        Vector3 dir = new Vector3(pos_x, gameobject.transform.position.y, pos_z);
        gameobject.transform.LookAt(dir);

        //由于有高低差等缺陷,因此采用距离而不是比较直接相等
        float distance = Vector3.Distance(dir, this.transform.position);

        if(distance > 0.9){
            //未到达,需要借助RigidBody移动
            Vector3 vec = data.walk_speed * gameobject.transform.forward;
            rigid.MovePosition(gameobject.transform.position + 10 * vec * Time.deltaTime);
        }
        else{
            //到达,转向
            Turn();
        }
    }

    private void Turn(){
        dirction = (dirction + 1) % 4;
        turn_sign = true;
    }

    private void OnCollisionEnter(Collision other) {
        if(other.gameObject.name.Contains("Wall")){
            //撞墙,转向
            Turn();
        }
    }
}

  • PatrolRunAction
    PatrolRunAction负责巡逻兵奔跑动作的管理,具体逻辑见代码注释。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class GuardRunAction : SSAction
{
    private GameObject player;        //玩家
    private PatrolData data;           //巡逻兵数据
    private Animator anim;            //巡逻兵动画
    private Rigidbody rigid;          //巡逻兵刚体

    public override void Start() {
        //初始化组件
        data = gameobject.GetComponent<PatrolData>();
        anim = gameobject.GetComponent<Animator>();
        rigid = gameobject.GetComponent<Rigidbody>();

        //设置跑动的动画
        anim.SetBool("isLive", true);
        anim.SetBool("isWalk", true);
        anim.SetBool("isRun", true);
    }

    public static GuardRunAction GetSSAction(GameObject player) {
        GuardRunAction action = CreateInstance<GuardRunAction>();
        action.player = player;
        return action;
    }

    public override void FixedUpdate() {
        //观察GuardData中的变量isLive(Reaction会改变这个变量)
        //若被灭活,则取消巡逻兵的动作(立刻回调)并取消动画
        if(!data.isLive){
            anim.SetBool("isLive", false);
            this.destroy = true;
            this.callback.SSActionEvent(this, SSActionEventType.Competeted, -1, this.gameobject);
            return;
        }
        
        //若不被灭活,则跑向玩家
        Run();

        //观察GuardData中的变量isRun(Reaction会改变这个变量)
        //如果玩家脱离该区域则继续巡逻
        if (!data.isRun) {
            this.destroy = true;
            this.callback.SSActionEvent(this, SSActionEventType.Competeted, 1, this.gameobject);
        }
    }

    private void Run(){
        //控制巡逻兵跑向玩家
        Vector3 dir = player.transform.position - player.transform.forward; //玩家偏后一点的位置
        gameobject.transform.LookAt(dir);   //转向
        Vector3 vec = data.run_speed * gameobject.transform.forward;    //设置移动向量
        rigid.MovePosition(gameobject.transform.position + 5 * vec * Time.deltaTime);   //借助RigidBody移动
    }
}

  • PatrolActionManager
    PatrolActionManager负责管理巡逻兵的两个动作之间的切换,即行走和奔跑,对上文所提及的两个动作类进行了调用,具体逻辑见下面代码。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class GuardActionManager : SSActionManager, ISSActionCallback {
    private GameObject player;    //玩家
    public SceneController sceneController;

    private void Start() {
        sceneController = SSDirector.GetInstance().CurrentScenceController as SceneController;
        sceneController.actionManager = this;
    }
    public void GuardWalk(GameObject guard, GameObject player) {
        //首先需要让巡逻兵走动
        this.player = player;
        GuardWalkAction walkAction = GuardWalkAction.GetSSAction(guard.transform.position);
        this.RunAction(guard, walkAction, this);
    }

    public void SSActionEvent(
        SSAction source, SSActionEventType events = SSActionEventType.Competeted,
        int intParam = 0, GameObject objectParam = null) {
        if (intParam == 0) {
            //巡逻状态返回,开始追逐
            GuardRunAction runAction = GuardRunAction.GetSSAction(player);
            this.RunAction(objectParam, runAction, this);
        } else if(intParam == 1){
            //追逐状态返回,开始巡逻
            GuardWalkAction walkAction = GuardWalkAction.GetSSAction(objectParam.GetComponent<PatrolData>().start_position);
            this.RunAction(objectParam, walkAction, this);
            sceneController.Record();
        }
        //对于其他状态,则是动作直接销毁,不再生成新动作
    }
}
   

四、游戏效果

游戏效果如下:

  • 游戏开始
    请添加图片描述

  • 游戏结束
    请添加图片描述

演示视频:智能巡逻兵 演示视频

项目地址:HW7

五、参考资料

[1]. unity官方文档

[2]. 师兄的博客

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值