【3D游戏编程与设计】七 模型与动画:智能巡逻兵

编写一个智能巡逻兵游戏

游戏设计要求:

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

程序设计要求:

  1. 必须使用订阅与发布模式传消息
  2. 工厂模式生产巡逻兵

项目架构

软件版本

项目使用的开发软件为Unity 3D 2020.1.4f1c1。

文件组织

在这里插入图片描述

项目的资源文件夹包括Assets和Packages两个子文件夹。其中,Packages子文件夹存储了系统自带的一些包,在这个项目中并没有特别使用。而Assets文件夹则存储了这次游戏项目使用的资源,场景,脚本,动画,预制等。其中,AxeyWorks,TileableBricksWallh和ToonSoldiers文件夹是游戏引用的一些资源包。而Scenes文件夹存储了游戏的场景(这个游戏中只有一个场景)。Scripts文件夹存储了游戏中使用的脚本。而Adnimator文件夹存储了游戏中使用的动画。Resources文件夹的Animator子文件夹则存储了游戏使用的Animator,而Materials子文件夹存储了游戏中使用的材料,Prefabs子文件夹存储了游戏中使用的预制。

其中,项目的脚本包括如下文件:
在这里插入图片描述

设计模式

项目主体框架采取MVC设计模式设计,在生产巡逻兵的管理上采用工厂模式,而在游戏对象之间进行消息通信时采用了订阅-发布模式。

MVC
MVC设计模式在之前Web 2.0课程中也有接触和学习实践过,在之前的牧师与魔鬼,打飞碟等游戏项目中也有进一步熟悉与实践。MVC界面是人机交互程序设计的一种经典架构模式。它把程序分为三个部分:

  • 模型(Model):数据对象及关系,包括游戏对象,空间组织关系等。游戏中的动画,材料,预制等属于模型部分。
  • 控制器(Controller):接受用户事件,控制模型的变化。在游戏中,每一个游戏场景都需要一个主控制器。控制器至少要能实现与玩家交互的接口,并且一般要实现和管理运动。游戏中的动作管理器,场景控制器,导演类等都属于控制器部分
  • 界面(View):显示模型,将人机交互事件交给控制器处理。其接收和处理输入事件,并进行相应GUI的渲染。游戏中的UserGUI等类属于界面部分。

而对于生产巡逻兵采用了工厂模式:
在这里插入图片描述

由于本次游戏需要产生很多巡逻兵,也需要创建玩家,我们需要特别关注如何低耦合地“创建和回收对象”,我们需要将对象的创建和使用分离。这样可以降低系统的耦合度,使得系统更容易扩展和维护,使用者也不需要关注对象的创建细节,对象的创建都由相应的工厂来完成。

主要关注点是“怎样创建对象”的创建型模式包括几种主要类型,分别为:

  • 单例(Singleton)模式:某个类只能生成一个实例,该类提供了一个全局访问点供外部获取该实例,其拓展是有限多例模式。
  • 原型(Prototype)模式:将一个对象作为原型,通过对其进行复制而克隆出多个和原型类似的新实例。
  • 工厂方法(FactoryMethod)模式:定义一个用于创建产品的接口,由子类决定生产什么产品。
  • 抽象工厂(AbstractFactory)模式:提供一个创建产品族的接口,其每个子类可以生产一系列相关的产品。
  • 建造者(Builder)模式:将一个复杂对象分解成多个相对简单的部分,然后根据不同需要分别创建它们,最后构建成该复杂对象。

在这个智能巡逻兵游戏中,根据游戏的规则和需要每一个巡逻区域都需要创建一个巡逻兵。为此,需要一个类来整合可以共享的代码来创建和回收(游戏重新开始或调整难度时可能需要回收巡逻兵)这些巡逻兵,也就是采用工厂类来创建巡逻兵。根据实验要求,本游戏中使用了一个场景单实例的工厂类PFactory,其负责创建具体的各个巡逻兵。这样就实现了巡逻兵的创建和使用相分离,增加新的巡逻兵种类和回收巡逻兵,也不需要在工厂类的外部进行较多的繁琐的修改。

由于智能巡逻兵游戏中涉及比较多的对象之间的通信,例如玩家进入一个巡逻区域后,会触发该区域的巡逻兵追击玩家。又或者玩家和一个巡逻兵碰撞后,需要及时通知到场景控制器,并进而通知到其他巡逻兵。如果没有采取恰当的设计模式来进行设计,则可能会导致对象之间的通信繁琐复杂, 容易出错,也不利于扩展和维护。为了解决这个问题,采用了订阅-发布模式。订阅-发布模式就是:订阅者订阅相应的事件调度中心(如同订阅一个公众号),当该事件触发时候,发布者将该事件发布到事件调度中心,由该事件调度中心调用所有订阅该调度中心的订阅者注入到该调度中心的处理代码。这样的设计就可以克服之前提到的问题,不需要发布者与每个订阅者直接通信,进而减少代码耦合度,方便游戏项目调试,扩展与维护。

设计思路

这次游戏中包括的对象主要是巡逻兵,玩家,障碍物,巡逻区域等,其中关键的活动对象是巡逻兵和玩家。本次实验重点关注游戏中的动画,我们需要对游戏中的活动对象,也就是巡逻兵和玩家设计与使用任务动画,使得其如同在现实世界中行走一样。

为了达到实验要求,这里设计了一个正方形地图,其中划分了若干个巡逻区域,每个巡逻区域中有一个巡逻兵。巡逻兵在玩家没有进入对应巡逻区域时,会沿着一定的凸多边形路线在区域内进行巡逻。而当玩家进入其对应巡逻区域后,会追击玩家。如果巡逻兵和玩家接触,则游戏结束,此时会弹出UI按钮让玩家选择是否重新开始,这时的巡逻兵和玩家应该停止移动。而当玩家每甩掉一个巡逻兵时,则可以获得分数,巡逻兵则会恢复巡逻。

前面提到这次游戏中只包括一个场景,也就是操纵玩家逃脱智能巡逻兵追击的场景。这个场景需要一个场景主控制器,其需要具体调用上面阐述的几个controllor。

导演类的职责包括:获取当前游戏的场景,控制场景运行、切换、入栈与出栈,暂停、恢复、退出,管理游戏全局状态,设定游戏的配置,设定游戏的全局视图等。其实际起到了最高的总体控制作用。导演类采取单体设计模式,其在整个游戏运行过程中始终只有唯一的一个实例对象。当前游戏中没有用到场景的切换,因此只是简单地设计了导演类对象,并没有在其中具体实现复杂的功能。

此外,我们还需要提供一个用户友好的GUI接口,其可以向用户展示目前的游戏状态,包括玩家的分数,而且可以及时反馈游戏是否结束信息,提供接口使得用户可以重新开始游戏。UserGUI类具体实现了这个功能。

游戏中的所有GameObject就是MVC架构中的Model,它们都分别受到对应的Controller的控制。上述的UserGUi类则属于View部分,该部分展示游戏状态,并且负责用户通过点击物体或按钮的形式与游戏交互。上面描述的对应场景的Controller,动作管理者,计分者,场景的主控制器,导演类则属于MVC架构中的Controller部分。这样的设计就符合MVC架构的设计。

项目地址

由于整个游戏文件夹过大,这里按照实验要求仅将Assets文件夹传到了公开的仓库上。仓库的链接为https://github.com/alphabstc/3D-PatrolSoilders。新建一个Unity 3D项目,按照下面的指引将仓库内容导入,将脚本拖到对应的对象上,应该可以创建出一个可以正常运行的游戏。

将游戏中对象做成预制

下载上述游戏需要的素材包之后,可以制作下面的材料:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

然后对下载素材中的人物模型进行相应修改和调整,制作成下面的预制:
在这里插入图片描述
其中,黄色衣服的是巡逻兵,绿色衣服的是玩家。

在这里插入图片描述
在这里插入图片描述
巡逻兵和玩家需要设计动画,其动画主要为跑步的动作。跑步的动画如下:
在这里插入图片描述
在这里插入图片描述
而动画的状态机则可以相应地设计如下:
在这里插入图片描述
此外,还将游戏的地图设计成了预制:
在这里插入图片描述
之后与之前的几个游戏项目类似,利用上面创建的预制,就可以使用脚本从空场景中创建出上面预制的游戏对象。

脚本分析与设计

GUI界面

首先,在UserGUI.cs中主要实现了UserGUI的图形界面类,其OnGUI函数具体完成了GUI界面各项文字和各个按钮的绘画与设置。其代码如下,具体分析详见注释:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class UserGUI : MonoBehaviour {

    private IUserAction action;

    private GUIStyle style1 = new GUIStyle();//声明字体
    private GUIStyle over_style = new GUIStyle();//声明字体
    void Start ()
    {
        action = GameDirector.GetInstance().CurrentScenceController as IUserAction;//从游戏导演类中获得当前场景控制器
        style1.normal.textColor = new Color(1, 1, 1, 1);//设置字体
        style1.fontSize = 16;
        over_style.fontSize = 25;//更大的字体
    }

    void Update()
    {
        float translationX = Input.GetAxis("Horizontal");//获取外设"Horizontal"轴的输入
        float translationZ = Input.GetAxis("Vertical");//获取外设"Vertical"轴的输入
        action.MovePlayer(translationX, translationZ);//移动玩家
    }
    private void OnGUI()
    {
		GUI.Label(new Rect(10, 5, 200, 50), "分数:", style1);//绘制分数
		GUI.Label(new Rect(55, 5, 200, 50), action.GetScore().ToString(), style1);
        if(action.GetGameover())//如果游戏结束
            if (GUI.Button(new Rect(Screen.width / 2 - 50, Screen.width / 2 - 150, 100, 50), "重新开始"))//绘制游戏结束和提示是否重新开始信息
            {
                action.Restart();//点击后重新开始
                return;
            }
        
    }
}

巡逻兵工厂

巡逻兵工厂是实现本次游戏项目的关键之一,该工厂是场景单实例的。正如之前所述,巡逻兵工厂类管理,创建和回收巡逻兵实例,其对外屏蔽巡逻兵实例的提取和回收细节。它可以根据传递的参数生成出对应的巡逻兵。其实现的一些细节可以参考上次游戏的飞碟工厂。代码如下,具体分析详见代码注释:

public class PFactory : MonoBehaviour
{
    private GameObject patrol = null;//临时对象  
    private List<GameObject> used = new List<GameObject>(); //存储巡逻兵的数据结构(列表)              
    private Vector3[] pos = new Vector3[9]; //存储巡逻兵位置的数组
    public FirstSceneController sceneControler;//场景控制器

    public List<GameObject> GetPatrols()
    {
        int[] pos_x = { -6, 4, 13 };//设置巡逻兵所在的3种x坐标位置
        int[] pos_z = { -4, 6, -13 };//设置巡逻兵所在的3种z坐标位置
        int index = 0;
        for(int i = 0;i < 3; ++i)//生成3*3个区域内每个区域的巡逻兵
        {
            for(int j = 0;j < 3; ++j)
            {
                pos[index] = new Vector3(pos_x[i], 5, pos_z[j]);//设置对应的三维坐标
                index++;//index后移
            }
        }
        for(int i = 0; i < 9; ++i)
        {
            patrol = Instantiate(Resources.Load<GameObject>("Prefabs/Patrol"));//载入巡逻兵预制
            patrol.transform.position = pos[i];//设置对应的坐标
            patrol.GetComponent<PatrolData>().sign = i + 1;//设置编号
            patrol.GetComponent<PatrolData>().start_position = pos[i];//设置开始位置
            used.Add(patrol);//加入到列表中
        }   
        return used;//返回列表
    }
}

巡逻兵参数

类似于之前飞碟这个类定义在文件PatrolData.cs中,其主要包含每个飞碟的位置,方向,大小,颜色,分数等参数,以及是属于哪一轮的飞碟。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class PatrolData : MonoBehaviour
{
    public int sign;
    public bool follow_player = false;
    public int wall_sign = -1;
    public GameObject player;
    public Vector3 start_position;
}

动作控制器

动作控制器的架构与之前牧师与魔鬼,打飞碟中的架构基本相同。其中,下面代码涉及的动作抽象类,动作管理者抽象类,其设计思想和代码与之前游戏项目中的基本相同。巡逻兵动作管理者类则继承了动作管理者抽象类,巡逻动作和追击动作则继承了动作抽象类。巡逻兵的巡逻动作是GoPatrolAction函数,函数中实现了巡逻按照任意的四边形的轨迹巡逻,动作销毁的条件是玩家进入了巡逻兵当前所在区域。巡逻兵的追击动作是PatrolFollowAction函数,该函数实现了巡逻兵向玩家移动并朝向玩家,动作销毁的条件是玩家已经不在该区域内了。动作管理者需要在合适的时候切换巡逻兵的动作,其切换动作通过销毁旧动作并创建新动作实现。巡逻动作结束的结束条件是玩家进入当前巡逻区域,需要追击玩家,所以调用了SSActionManager中的回调函数,用回调函数来进行追击动作。而当玩家离开巡逻兵控制的区域后,巡逻兵需要重新进入巡逻状态,也需要调用SSActionManager中的回调函数,从当前位置和方向继续开始巡逻。除此之外,SSActionManager还实现了在游戏结束后销毁所有动作,使得巡逻兵不再移动。游戏刚开始时,场景控制器调用PatrolActionManager中的方法,让巡逻兵进行运动。当游戏结束的时候,调用DestroyAllAction方法让巡逻兵停止巡逻。代码如下,具体分析详见注释:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class SSAction : ScriptableObject//动作的抽象类 与之前游戏的类似
{
	public bool enable = true;  
	public bool destroy = false;   
	public GameObject gameobject;  
	public Transform transform;      
	public ISSActionCallback callback; 

	protected SSAction() { }
	public virtual void Start()
	{
		throw new System.NotImplementedException();
	}
	public virtual void Update()
	{
		throw new System.NotImplementedException();
	}
}

public class SSActionManager : MonoBehaviour, ISSActionCallback//动作管理者类
{
	private Dictionary<int, SSAction> actions = new Dictionary<int, SSAction>(); //使用了字典类型,根据int类型映射到对应的动作
	private List<SSAction> waitingAdd = new List<SSAction>(); //等待加入列表
	private List<int> waitingDelete = new List<int>(); //等待删除列表        

	protected void Update()//更新
	{
		foreach (SSAction ac in waitingAdd)//遍历等待加入列表
		{
			actions[ac.GetInstanceID()] = ac;//设置映射关系 设置id映射到的动作    
		}
		waitingAdd.Clear();//遍历了一遍等待加入列表 清空等待加入列表

		foreach (KeyValuePair<int, SSAction> kv in actions)//遍历actions
		{
			SSAction ac = kv.Value;
			if (ac.destroy)//如果已经销毁 
			{
				waitingDelete.Add(ac.GetInstanceID());//id加入等待删除列表
			}
			else if (ac.enable)//否则进行更新
			{
				ac.Update();
			}
		}

		foreach (int key in waitingDelete)//遍历等待删除列表
		{
			SSAction ac = actions[key];//从actions中删除
			actions.Remove(key);
			Destroy(ac);//销毁
		}
		waitingDelete.Clear();//遍历了一遍等待删除列表 清空等待删除列表
	}

	public void RunAction(GameObject gameobject, SSAction action, ISSActionCallback manager)//执行动作
	{
		action.gameobject = gameobject;//设置相应gameobject和transform
		action.transform = gameobject.transform;
		action.callback = manager;//设置回调者
		waitingAdd.Add(action);//等待加入动作
		action.Start();//执行
	}

	public void SSActionEvent(SSAction source, int intParam = 0, GameObject objectParam = null)
	{
		if(intParam == 0)
		{
			PatrolFollowAction follow = PatrolFollowAction.GetSSAction(objectParam.gameObject.GetComponent<PatrolData>().player);//设置跟随玩家
			this.RunAction(objectParam, follow, this);//执行动作
		}
		else
		{
			GoPatrolAction move = GoPatrolAction.GetSSAction(objectParam.gameObject.GetComponent<PatrolData>().start_position);//设置巡逻
			this.RunAction(objectParam, move, this);//执行动作
			Singleton<GameEventManager>.Instance.PlayerEscape();//玩家逃脱 增加分数
		}
	}


	public void DestroyAll()//删除所有动作
	{
		foreach (KeyValuePair<int, SSAction> kv in actions)//遍历actions字典
		{
			SSAction ac = kv.Value;
			ac.destroy = true;//设置删除
		}
	}
}

public class PatrolActionManager : SSActionManager//处理巡逻兵的动作的管理者
{
	private GoPatrolAction go_patrol;  //巡逻动作
	public void GoPatrol(GameObject patrol)//巡逻
	{
		go_patrol = GoPatrolAction.GetSSAction(patrol.transform.position);//获得巡逻动作
		this.RunAction(patrol, go_patrol, this);//执行该巡逻动作
	}
	public void DestroyAllAction(){DestroyAll();}//删除所有动作
}

public class GoPatrolAction : SSAction//巡逻动作类 继承了动作抽象类
{
	private enum Dirction { E, N, W, S };//方向枚举类型
	private float pos_x, pos_z; //x和z坐标
	private float move_length; //移动长度
	private float move_speed = 1.2f; 
	private bool move_sign = true;//移动标志
	private Dirction dirction = Dirction.E;//移动方向 
	private PatrolData data;//巡逻兵参数

	private GoPatrolAction() { }
	public static GoPatrolAction GetSSAction(Vector3 location)
	{
		GoPatrolAction action = CreateInstance<GoPatrolAction>();//创建巡逻动作实例
		action.pos_x = location.x;//设置当前坐标
		action.pos_z = location.z;
		action.move_length = Random.Range(4, 8);//随机移动长度
		return action;//返回动作
	}
	public override void Update()//
	{
		if (transform.localEulerAngles.x != 0 || transform.localEulerAngles.z != 0)
			transform.localEulerAngles = new Vector3(0, transform.localEulerAngles.y, 0); //调整欧拉角 使得巡逻兵垂直于地面
		if (transform.position.y != 0)
			transform.position = new Vector3(transform.position.x, 0, transform.position.z);//调整坐标 使得巡逻兵在地面上
		Gopatrol();//巡逻
		if (data.follow_player && data.wall_sign == data.sign)//玩家所在区域等于当前巡逻兵所在区域
		{
			this.destroy = true;//销毁当前动作
			this.callback.SSActionEvent(this,0,this.gameobject);//触发切换动作事件
		}
	}
	public override void Start(){data  = this.gameobject.GetComponent<PatrolData>();}//获得巡逻兵参数
	void Gopatrol()//巡逻
	{
		if (move_sign)//根据是否可以移动的标志
		{
			switch (dirction)//根据移动方向修改z和z坐标
			{
			case Dirction.E:
				pos_x -= move_length;

				break;
			case Dirction.N:
				pos_z += move_length;

				break;
			case Dirction.W:
				pos_x += move_length;

				break;
			case Dirction.S:
				pos_z -= move_length;

				break;
			}
			move_sign = false;//移动完成
		}
		this.transform.LookAt(new Vector3(pos_x, 0, pos_z));//设置朝向
		float distance = Vector3.Distance(transform.position, new Vector3(pos_x, 0, pos_z));//计算距离
		if (distance > 0.9){transform.position = Vector3.MoveTowards(this.transform.position, new Vector3(pos_x, 0, pos_z), move_speed * Time.deltaTime);//超过阈值则移动相应距离
		}else
		{
			dirction = dirction + 1;//切换到下一个方向
			if(dirction > Dirction.S)dirction = Dirction.E;
			move_sign = true;//需要移动
		}
	}
}

public class PatrolFollowAction : SSAction//跟随动作 也继承了动作抽象类
{
	private float speed = 2f;//速度
	private GameObject player;//玩家
	private PatrolData data;//巡逻兵参数
	private PatrolFollowAction() { }
	public static PatrolFollowAction GetSSAction(GameObject player)//获得动作
	{
		PatrolFollowAction action = CreateInstance<PatrolFollowAction>();//创建跟随动作实例
		action.player = player;//设置玩家
		return action;//返回动作
	}
	public override void Update()//更新
	{
		if (transform.localEulerAngles.x != 0 || transform.localEulerAngles.z != 0)
			transform.localEulerAngles = new Vector3(0, transform.localEulerAngles.y, 0);  //调整欧拉角 使得巡逻兵垂直于地面        
		if (transform.position.y != 0)
			transform.position = new Vector3(transform.position.x, 0, transform.position.z);//调整坐标 使得巡逻兵在地面上
		Follow();//跟随
		if (!data.follow_player || data.wall_sign != data.sign)//如果玩家走出当前区域
		{
			this.destroy = true;//销毁当前动作
			this.callback.SSActionEvent(this,1,this.gameobject);//触发切换动作事件
		}
	}
	public override void Start(){data = this.gameobject.GetComponent<PatrolData>();}//初始化
	void Follow()//跟随
	{
		transform.position = Vector3.MoveTowards(this.transform.position, player.transform.position, speed * Time.deltaTime);//向玩家移动
		this.transform.LookAt(player.transform.position);//朝向玩家
	}
}

主场景控制器

主场景控制器具体实现了各个接口中的函数,完成了预制的加载和玩家的移动。游戏开始时其获得游戏导演类并将导演类当前场景控制器设置为自己,之后获得巡逻兵工厂单体实例和动作管理者类,计分者类并载入资源。游戏过程中则检查巡逻兵所在区域和玩家所在区域,和玩家区域相同的巡逻兵追击玩家,和玩家区域不同的巡逻兵则进行巡逻。同时其利用了定义的单例模板类,保证了巡逻兵工厂是单实例的。函数OnEnable()和OnDisable()中则实现了发布-订阅模式的操作。MovePlayer(),GetScore()与GetGameover()等函数则是与UserGUI交互的函数。代码如下,具体分析详见代码注释:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;

public class FirstSceneController : MonoBehaviour, IUserAction, ISceneController
{
    public PFactory patrol_factory;
    public ScoreRecorder recorder;
    public PatrolActionManager action_manager;
    public int wall_sign = 4;
    public GameObject player;
    public float player_speed = 30; 
	public float rotate_speed = 10; 
    private List<GameObject> patrols;
    private bool game_over = false;
    public Vector3 movement;
	public int GetScore(){return recorder.GetScore();}//获得分数
	public bool GetGameover(){return game_over;}//获得游戏是否结束的状态
	public void Restart(){SceneManager.LoadScene(0);}//重新开始
	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;action_manager.DestroyAllAction();}//游戏结束 则销毁所有动作
    void Update()//更新
    {
        for (int i = 0; i < patrols.Count; i++)patrols[i].gameObject.GetComponent<PatrolData>().wall_sign = wall_sign;//patrols[i]获得玩家所在区域
		for (int i = 0; i < patrols.Count; i++)
			if (patrols [i].gameObject.GetComponent<PatrolData> ().sign == patrols [i].gameObject.GetComponent<PatrolData> ().wall_sign) {//巡逻兵与玩家在相同区域
				patrols [i].gameObject.GetComponent<PatrolData> ().follow_player = true;//开始追击
				patrols [i].gameObject.GetComponent<PatrolData> ().player = player;
			} else {
				patrols [i].gameObject.GetComponent<PatrolData>().follow_player = false;//停止追击
				patrols [i].gameObject.GetComponent<PatrolData>().player = null;
			}
		
    }
    void Start()
    {
		wall_sign = 4;
        GameDirector director = GameDirector.GetInstance();//获得游戏导演类
        director.CurrentScenceController = this;//设置导演类当前场景控制器为自己
        patrol_factory = Singleton<PFactory>.Instance;//获得巡逻兵工厂单体实例
        action_manager = gameObject.AddComponent<PatrolActionManager>() as PatrolActionManager;//获得动作管理者类
        LoadResources();//载入资源
        recorder = Singleton<ScoreRecorder>.Instance;//获得计分者类
    }

    public void LoadResources()//载入资源
    {
        Instantiate(Resources.Load<GameObject>("Prefabs/Plane"));//载入地图
        player = Instantiate(Resources.Load("Prefabs/Player"), new Vector3(0, 9, 0), Quaternion.identity) as GameObject;//载入玩家
        patrols = patrol_factory.GetPatrols();//载入巡逻兵
        for (int i = 0; i < patrols.Count; i++) action_manager.GoPatrol(patrols[i]);//动作管理者操纵巡逻兵
    }
	
    public void MovePlayer(float translationX, float translationZ)//移动玩家
    {
        if(!game_over)//游戏结束后则不可以移动
        {
			Vector3 direction = new Vector3(translationX,0,translationZ).normalized;//方向归一化
			player.transform.position = Vector3.MoveTowards(player.transform.position, player.transform.position+direction, player_speed * Time.deltaTime);//移动玩家
			player.transform.LookAt(player.transform.position+direction);//朝向移动的方向
        }
    }
}

其中单例模式实现如下:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Singleton<T> : MonoBehaviour where T : MonoBehaviour//泛型编程 适用于任何类
{
    protected static T instance;
    public static T Instance
    {
        get
        {
            if (instance == null)//实例不存在
            {
                instance = (T)FindObjectOfType(typeof(T));//获得对应实例    
            }
            return instance;//返回
        }
    }
}

游戏事件管理者

该游戏事件管理者类GameEventManager是通过订阅-发布模式实现的,订阅者订阅该类中声明的事件,当其他发布者类需要发布事件时,会调用GameEventManager的方法发布消息。而主场景控制器是本游戏项目中的订阅者,其订阅了GameEventManager中的事件,如果在场景中有相应事件发生,那么主场景控制器就会执行相应的方法,在本游戏项目中则是得分函数与游戏结束函数。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class GameEventManager : MonoBehaviour//事件管理器
{
    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();//处理游戏结束
        }
    }
}

碰撞器

首先是AreaCollider类,其处理了玩家和区域边界的碰撞,完成了对玩家进入与离开区域的处理。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class AreaCollider : MonoBehaviour
{
    public int sign = 0;
    FirstSceneController sceneController;
    private void Start(){//获得对应主场景控制器
    	sceneController = GameDirector.GetInstance().CurrentScenceController as FirstSceneController;
    }
    void OnTriggerEnter(Collider collider){
    	if (collider.gameObject.name == "Player(Clone)")//区域与玩家碰撞
    		sceneController.wall_sign = sign;//设置玩家所在区域
    }
}

然后是PlayerCollider类,其处理了玩家和巡逻兵的碰撞,碰撞将会触发游戏结束。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class PlayerCollider : MonoBehaviour{
	void OnCollisionEnter(Collision other){
		if (other.gameObject.name == "Player(Clone)")//巡逻兵与玩家碰撞
			Singleton<GameEventManager>.Instance.PlayerGameover();//游戏结束
		}
	}
}

计分者

这个类的功能比较简单,其管理游戏的分数score,并实现了对score的get和set方法GetScore和AddScore。其被主场景控制器调用。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class ScoreRecorder : MonoBehaviour
{
    public FirstSceneController sceneController;
    public int score = 0; 
    void Start() {
    	sceneController = (FirstSceneController)GameDirector.GetInstance().CurrentScenceController;//获得主场景控制器
    	sceneController.recorder = this;//设置主场景控制器的计分者
    }
    public int GetScore(){return score;}
    public void AddScore(){score++;}
}

导演类

这个类起到了导演的功能。其是整个游戏中最高的控制器,由初始场景的主控制器调用载入。导演类采用单体设计模式,在整个游戏过程中仅有一个实例对象。当前游戏中没有用到场景的切换,因此只是简单地设计了导演类对象,并没有在其中具体实现复杂的功能。

public class GameDirector : System.Object
{
    private static GameDirector _instance;             //导演类的实例
    public ISceneController CurrentScenceController { get; set; }
    public static GameDirector GetInstance()
    {
        if (_instance == null)
        {
            _instance = new GameDirector();//获得导演类实例
        }
        return _instance;
    }
}

调整摄像头

调整摄像头的位置和姿态,使得其可以以比较好的视角看见游戏场景。

在这里插入图片描述
在这里插入图片描述

这样就完成了项目的配置。

游戏效果

视频链接:https://www.bilibili.com/video/BV12r4y1F7iz/

项目仓库地址https://github.com/alphabstc/3D-PatrolSoilders也含有可以下载的演示视频和动图,可以下载到本地后观看。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值