【3D游戏编程与设计】六 物理系统与碰撞:鼠标打飞碟(Hit UFO)改进版
改进鼠标打飞碟(Hit UFO)游戏:
游戏内容要求
- 游戏有 n 个 round,每个 round 都包括10 次 trial;
- 每个 trial 的飞碟的色彩、大小、发射位置、速度、角度、同时出现的个数都可能不同。它们由该 round 的 ruler 控制;
- 每个 trial 的飞碟有随机性,总体难度随 round 上升;
- 鼠标点中得分,得分规则按色彩、大小、速度不同计算,规则可自由设定。
游戏设计要求
- 按 adapter模式 设计图修改飞碟游戏
- 使它同时支持物理运动与运动学(变换)运动
设计模式优化
结构型模式描述如何将类或对象按某种布局组成更大的结构的一般方式。
结构型模式可以分为代理(Proxy)模式,适配器(Adapter)模式,桥接(Bridge)模式,装饰(Decorator)模式,外观(Facade)模式,享元(Flyweight)模式,组合(Composite)模式等七种。其中,适配器(Adapter)模式是指将一个类的接口转换成客户希望的另外一个接口,使得原本由于接口不兼容而不能一起工作的那些类能一起工作。
在上一版本的打飞碟游戏中,我们使用了坐标变换和运动学理论推导的公式实现了鼠标打飞碟游戏,我们使用了CCActionManager类完成了这些运动学坐标变换的管理。由于我们刚刚学习了Unity的物理引擎部分,更深入地了解了Unity的物理系统与碰撞。我们考虑也可以从动力学的角度,通过刚体和力等物理效果的操纵和设置达到打飞碟游戏的效果。根据作业要求,我们需要同时支持运动学角度实现的打飞碟游戏,也要支持动力学角度实现的打飞碟游戏。我们需要采用一个新的PhysisActionManager类来进行刚体和力等物理效果的操纵和管理。
如果我们直接根据CCActionManager类和PhysisActionManager类分别实现鼠标打游戏的其他部分。那么一方面,实现出来的游戏相当于两个不同的游戏无机地拼接在一起,在逻辑层面并不是一个好的设计;另一方面,实现出的游戏重复的代码和耦合度高的代码很多,这样不利于游戏的可维护性和可扩展性;此外,这样的工作量也比较大,没有很好地挖掘整理出游戏中共用的代码模块。
因此我们可以考虑采用适配器(Adapter)模式。我们可以在不放弃上一个游戏的CCActionManager类基础上,重新设计新的动力学PhysisActionManager类,并将CCActionManager类和PhysisActionManager类用一个统一的接口类IActionManager管理。而游戏的其他部分,则可以通过与IActionManager接口类交互,实现出游戏的效果。这就采用了适配器模式,将两种不同的动作管理类统一在了一个动作管理接口类下面,使得游戏重复代码减少,耦合度降低,可扩展性和可维护性都得到了明显增强,开发这个游戏也变得更游戏了。游戏采用的系统类图如下:
项目架构
软件版本
项目使用的开发软件为Unity 3D 2020.1.4f1c1。
文件组织

项目的资源文件夹包括Assets和Packages两个子文件夹。其中,Packages子文件夹存储了系统自带的一些包,在这个项目中并没有特别使用。而Assets文件夹则存储了这次游戏项目使用的资源,场景,脚本等。其中的Scripts文件夹存储了游戏中使用的脚本。而Resources文件夹存储了游戏中用到的材料和预制对象:其子文件夹Textures存放了游戏中使用的texture贴图;其子文件夹Materials存放了游戏中使用的材料;其子文件夹Prefabs存放了游戏中设计的飞碟与粒子系统(飞碟碎片溅射效果)预制。而Scenes文件夹存储了游戏的场景(这个游戏中只有一个场景)。
其中,项目的脚本包括如下文件:

设计思路
游戏项目除了采用适配器模式之外,还采用MVC设计模式和工厂模式设计。其中MVC设计模式在之前Web 2.0课程中也有接触和学习实践过,在之前的牧师与魔鬼游戏中也有进一步熟悉与实践。MVC界面是人机交互程序设计的一种经典架构模式。它把程序分为三个部分:
- 模型(Model):数据对象及关系,包括游戏对象,空间组织关系等。
- 控制器(Controller):接受用户事件,控制模型的变化。在游戏中,每一个游戏场景都需要一个主控制器。控制器至少要能实现与玩家交互的接口,并且一般要实现和管理运动。
- 界面(View):显示模型,将人机交互事件交给控制器处理。其接收和处理输入事件,并进行相应GUI的渲染。
由于本次游戏需要产生和回收很多飞碟,我们需要特别关注如何低耦合地“创建和回收对象”,我们需要将对象的创建和使用分离。这样可以降低系统的耦合度,使得系统更容易扩展和维护,使用者也不需要关注对象的创建细节,对象的创建都由相应的工厂来完成。
主要关注点是“怎样创建对象”的创建型模式包括几种主要类型,分别为:
- 单例(Singleton)模式:某个类只能生成一个实例,该类提供了一个全局访问点供外部获取该实例,其拓展是有限多例模式。
- 原型(Prototype)模式:将一个对象作为原型,通过对其进行复制而克隆出多个和原型类似的新实例。
- 工厂方法(FactoryMethod)模式:定义一个用于创建产品的接口,由子类决定生产什么产品。
- 抽象工厂(AbstractFactory)模式:提供一个创建产品族的接口,其每个子类可以生产一系列相关的产品。
- 建造者(Builder)模式:将一个复杂对象分解成多个相对简单的部分,然后根据不同需要分别创建它们,最后构建成该复杂对象。
在这个飞碟游戏中,每一轮都有很多种不同种类的飞碟,根据游戏的规则和需要,代码中会有很多处地方都需要创建飞碟。为此,需要一个类来整合这些可以共享的代码来创建这些飞碟,也就是采用工厂类来创建飞碟。根据实验要求,本游戏中使用了一个场景单实例的工厂类DiskFactory,其负责创建和回收各种具体的飞碟。这样就实现了飞碟的创建和使用相分离,增加新的飞碟种类,也不需要在工厂类的外部进行较多的繁琐的修改。
这次游戏中包括的对象主要是各种飞碟。本次实验重点关注用户与游戏世界的交互,用户的输入设备,在本游戏中主要是鼠标,也是这个游戏中需要重点关注的对象。我们可以考虑玩家与游戏中对象交互的各种动作。经过考察与分析,我们需要对飞碟和关卡都定义controllor,此外类似之前牧师与魔鬼的框架,需要定义管理动作的controllor,此外就是管理场景的controllor。
前面提到这次游戏中只包括一个场景,也就是产生飞碟和玩家击打飞碟的场景(不同的关卡都在这个场景中设计,不同关卡产生飞碟的重力,初始速度,频率,分数等参数不同)。这个场景需要一个场景主控制器,其需要具体调用上面阐述的几个controllor。
导演类的职责包括:获取当前游戏的场景,控制场景运行、切换、入栈与出栈,暂停、恢复、退出,管理游戏全局状态,设定游戏的配置,设定游戏的全局视图等。其实际起到了最高的总体控制作用。导演类采取单体设计模式,其在整个游戏运行过程中始终只有唯一的一个实例对象。当前游戏中没有用到场景的切换,因此只是简单地设计了导演类对象,并没有在其中具体实现复杂的功能。
此外,我们还需要提供一个用户友好的GUI接口,其可以向用户展示目前的游戏状态,包括玩家的生命,分数,轮数,而且可以及时反馈游戏胜利、失败信息,提供接口使得用户可以重新开始游戏。UserGUI类具体实现了这个功能。
游戏中的所有GameObject就是MVC架构中的Model,它们都分别受到对应的Controller的控制。上述的UserGUi类则属于View部分,该部分展示游戏状态,并且负责用户通过点击物体或按钮的形式与游戏交互。上面描述的对应飞碟和关卡的Controller,场景的主控制器,导演类则属于MVC架构中的Controller部分。这样的设计就符合MVC架构的设计。
游戏素材
游戏素材和上一个项目的基本相同,没有使用其他的新的素材。根据实验要求,游戏中所有对象都必须由代码生成,初始不能有游戏对象出现在游戏中,因此需要创建飞碟和粒子系统的预制。创建飞碟和粒子系统预制的操作也相同,可以参考上一个游戏项目中的操作。
对于飞碟的预制,需要在飞碟中加入rigidbody刚体组件,然后将Use Gravity设置为false。(后面根据实际情况用脚本进一步调整)

项目地址
由于整个游戏文件夹过大,这里按照实验要求仅将Assets文件夹传到了公开的仓库上。仓库的链接为https://github.com/alphabstc/Hit-UFO-new。新建一个Unity 3D项目,按照下面的指引将仓库内容导入,将脚本拖到对应的对象上,应该可以创建出一个可以正常运行的游戏。
脚本分析与设计
GUI界面
首先,声明了如下的接口类,其声明了GUI界面需要使用的虚函数。之后,场景控制器会具体实现这些函数,并提供给GUI使用。相比上一个版本的游戏设计,其添加了游戏模式设置等函数。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public interface ISceneControllor{
void LoadResources ();
}
public interface IUserAction{
void Hit (Vector3 pos);
void Restart ();//重新开始
int GetScore();//获得分数
int GetRound();//获得当前轮数
void GameOver();//游戏结束
bool isCounting();//是否在计时
int getEmitTime();//获得开始时间 用于开场倒计时
void modeSet(bool flag);//设置是运动学模式还是物理学模式
void gameBegin();//开始游戏
bool getModeSetting();//获得模式
void setting(float speed,GameObject explosion);//设置速度和爆炸效果
}
之后,在UserGUI.cs中主要实现了UserGUI的图形界面类,其OnGUI函数具体完成了GUI界面各项文字和各个按钮的绘画与设置。游戏开始前,其提供界面让用户选择游戏模式,而进入游戏过程中,其会提示当前分数、生命值、轮数等信息。其代码如下,具体分析详见注释:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class UserGUI : MonoBehaviour
{
private IUserAction action;
public int life = 10;//玩家生命 也就是还可以错过的飞碟数目
GUIStyle text_style = new GUIStyle();
GUIStyle text_style2 = new GUIStyle();
void Start ()
{
action = GameDirector.GetInstance().CurrentSceneControllor as IUserAction;//获得当前的场景控制器
}
void OnGUI ()
{
//设置字体
text_style.normal.textColor = new Color(1, 1, 1, 1);
text_style.fontSize = 16;
//设置另一种字体
text_style2.normal.textColor = new Color(1, 1, 1, 1);
text_style2.fontSize = 100;
if (action.getModeSetting()) {//游戏开始界面 选择游戏模式
GUI.Label(new Rect(Screen.width / 2 - 40, Screen.height / 2 - 100, 50, 50), "游戏模式", text_style);
if (GUI.Button(new Rect(Screen.width / 2 - 55, Screen.height / 2 + 20, 100, 50), "物理学模式"))
{
action.modeSet (true);
action.gameBegin ();
return;
}
if (GUI.Button(new Rect(Screen.width / 2 - 55, Screen.height / 2 - 50, 100, 50), "运动学模式"))
{
action.modeSet (false);
action.gameBegin ();
return;
}
} else {//已经选择了游戏模式
if (action.isCounting ()) {//开场倒计时的提示信息绘制
GUI.Label(new Rect(Screen.width / 2 - 40, Screen.height / 2 - 100, 50, 50), action.getEmitTime().ToString(), text_style2);
} else {//游戏中游戏状态信息绘制
if (Input.GetButtonDown("Fire1"))//鼠标点击
{
Vector3 pos = Input.mousePosition;//获得鼠标位置
action.Hit(pos);//触发鼠标点击事件
}
GUI.Label(new Rect(10, 5, 200, 50), "score:", text_style);//分数信息
GUI.Label(new Rect(60, 5, 200, 50), action.GetScore().ToString(), text_style);
GUI.Label(new Rect(10, 30, 50, 50), "hp:", text_style);//生命值信息
for (int i = 0; i < life; i++)
{
GUI.Label(new Rect(40 + 10 * i, 30, 50, 50), "X", text_style);
}
if (life == 0)//游戏结束
{
GUI.Label(new Rect(Screen.width / 2 - 50, Screen.width / 2 - 300, 100, 100), "GameOver!", text_style);
if (GUI.Button(new Rect(Screen.width / 2 - 60, Screen.width / 2 - 250, 100, 50), "Restart"))
{
action.Restart();//重新开始
return;
}
action.GameOver();//执行游戏结束
}
//当前轮数的信息
GUI.Label(new Rect(10, 55, 200, 50), "Round:", text_style);
GUI.Label(new Rect(64, 55, 200, 50), action.GetRound().ToString(), text_style);
}
}
}
public void ReduceBlood()//减少生命值
{
if(life > 0)
life--;
}
}
飞碟工厂
飞碟工厂使用带缓存的工厂模式管理不同飞碟的生产与回收。正如之前所述,飞碟工厂类是单实例的,其管理,创建和回收飞碟实例,其对外屏蔽飞碟实例的提取和回收细节。它可以根据当前轮数的不同生产出不同种类和属性的飞碟,然后对于需要回收的(被点中或者飞出场景外)飞碟就可以进行回收。飞碟工厂从仓库中获取这种类型的飞碟,如果仓库中没有这种类型的,则新的实例化一个飞碟,然后添加到正在使用的飞碟列表中。这样就可以保证资源更能被有效利用,因为每次动态申请和释放飞碟都需要大量资源,这样就可以避免反复申请和释放飞碟。代码如下,具体分析详见代码注释:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class DiskFactory : MonoBehaviour//飞碟工厂类
{
public GameObject disk_prefab = null; //飞碟预制
private List<DiskData> used = new List<DiskData>(); //正在使用的
private List<DiskData> free = new List<DiskData>(); //已经被释放的
public GameObject GetDisk(int round)//产生飞碟
{
float start_y = -10f; //开始的y坐标
string tag;
disk_prefab = null;//预制件指针
if (round == 1)//第一轮
{
tag = "diskG";;
}
else if(round == 2)//第二轮
{
tag = "diskY";
}
else//其他轮
{
tag = "diskR";
}
for(int i=0;i<free.Count;i++)//检查是否有空闲的飞碟
{
if(free[i].type == tag)//是同一类型的
{
disk_prefab = free[i].gameObject;//使用这个飞碟
free.Remove(free[i]);//被使用
break;
}
}
if(disk_prefab == null)//没有找到之前被回收的 需要新创建
{
if (tag == "diskG")//绿色
{
disk_prefab = Instantiate(Resources.Load<GameObject>("Prefabs/diskG"), new Vector3(0, start_y, 0), Quaternion.identity);
}
else if (tag == "diskY")//黄色
{
disk_prefab = Instantiate(Resources.Load<GameObject>("Prefabs/diskY"), new Vector3(0, start_y, 0), Quaternion.identity);
disk_prefab.GetComponent<DiskData> ().score = 2;//分数更高
}
else//红色
{
disk_prefab = Instantiate(Resources.Load<GameObject>("Prefabs/diskR"), new Vector3(0, start_y, 0), Quaternion.identity);
disk_prefab.GetComponent<DiskData> ().score = 3;//分数最高
}
float ran_x = Random.Range(-1f, 1f) < 0 ? -1 : 1;//随机x坐标
disk_prefab.GetComponent<MeshRenderer> ().material.color = disk_prefab.GetComponent<DiskData>().color;//获得对应的颜色
disk_prefab.GetComponent<DiskData>().direction = new Vector3(ran_x, start_y, 0);//设置方向
disk_prefab.GetComponent<DiskData> ().type = tag;//设置tag
disk_prefab.transform.localScale = disk_prefab.GetComponent<DiskData>().scale;//设置大小范围
}
used.Add(disk_prefab.GetComponent<DiskData>());//增加到使用的飞碟中去
return disk_prefab;
}
public void FreeDisk(GameObject disk)//回收飞碟
{
for(int i = 0;i < used.Count; i++)
{
if (disk.GetInstanceID() == used[i].gameObject.GetInstanceID())//正在索引的飞碟为正好需要的飞碟
{
used[i].gameObject.SetActive(false);//设置其被回收
free.Add(used[i]);//增加到回收中的去
used.Remove(used[i]);//从正在使用中的移除
break;
}
}
}
}
飞碟参数
这个类定义在文件DiskDate.cs中,其主要包含每个飞碟的位置,方向,大小,颜色,分数等参数,以及是属于哪一轮的飞碟。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class DiskData : MonoBehaviour
{
public int score = 1; //分数
public Color color = Color.white; //颜色
public Vector3 direction; //方向
public Vector3 scale = new Vector3(3, 0.3f, 3); //大小
public string type;//标签
}
动作控制器适配器
在interController.cs中除了定义上面所述的IUserAction类用于管理用户UI,还定义了本游戏的核心——动作控制器适配器类
IActionManager。这个动作适配器类起到了场景控制器与动作控制器之间接口的作用。这样就可以通过场景控制器统一地通知适配器类来进一步根据情况调用运动学或物理学的动作控制器。实现代码如下:
public interface IActionManager
{
void UFOFly(GameObject disk, float angle, float power,bool isPhy);
}
其中UFOFly函数的第一项参数表示需要操作的飞碟对象,第二项参数表示飞碟的角度,第三项参数表示飞碟的能量(初速度),最后一项flag代表适配器选择物理学或运动学进行UFO运动,true代表物理学,false代表运动学。
上面定义了适配器接口(抽象类)。现在考虑具体实现适配器类,这个具体的适配器类继承了IActionManager这个接口,其有能力调用物理动作管理器和运动学动作管理器。当场景控制器通知适配器需要用什么动作模式进行运动时,便可以通过flag进行动作模式的选择。实现如下,具体分析详见代码注释:
public class ActionManagerAdapter : MonoBehaviour,IActionManager//动作控制器适配器类
{
public FlyActionManager action_manager;//运动学动作控制器
public PhysicsFlyActionManager phy_action_manager;//物理学动作控制器
public void UFOFly(GameObject disk, float angle, float power,bool flag) {//根据flag调用不同的动作控制器进行动作控制
if(flag) phy_action_manager.UFOfly(disk, angle, power);
else action_manager.UFOfly(disk, angle, power);
}
void Start ()
{//获得对应动作控制器
action_manager = gameObject.AddComponent<FlyActionManager>() as FlyActionManager;
phy_action_manager = gameObject.AddComponent<PhysicsFlyActionManager>() as PhysicsFlyActionManager;
}
}
运动学动作控制器
运动学动作控制器类除了参数有些许调整外,基本和上次游戏项目中的实现相同。运动学动作控制器继承了动作控制器接口,其使用场景控制器中的FlyActionManager 类的函数,然后在FlyActionManager 类中调用UFOFlyAction类进行每一帧对飞碟位置的渲染更新即可实现飞碟飞的动作。具体来说,需要在UFOFlyAction类中给飞碟一个初始速度(包括大小和方向),然后根据之前离散仿真引擎所学知识和现实生活中的物理定律,飞碟每一帧计算下一帧做有向下重力加速度的位置,然后进行赋值即可实现的飞行动作。最后当飞出场景外或者被玩家击中时就需要等待场景控制器和飞碟工厂进行配合回收飞碟。代码如下,具体分析详见注释:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class GameAction : ScriptableObject//动作的抽象类
{
public bool enable = true;
public bool destroy = false;
public GameObject gameobject;
public Transform transform;
public ISSActionCallback callback;
protected GameAction() { }
public virtual void Start()
{
throw new System.NotImplementedException();
}
public virtual void Update()
{
throw new System.NotImplementedException();
}
}
public enum SSActionEventType : int { Started, Competeted }//枚举类型
public interface ISSActionCallback//用于动作回调的接口
{
void SSActionEvent(GameAction source, SSActionEventType events = SSActionEventType.Competeted,
int intParam = 0, string strParam = null, Object objectParam = null);
}
public class SSActionManager : MonoBehaviour, ISSActionCallback//动作管理者类
{
private Dictionary<int, GameAction> actions = new Dictionary<int, GameAction>(); //使用了字典类型,根据int类型映射到对应的动作
private List<GameAction> waitingAdd = new List<GameAction>(); //等待加入列表
private List<int> waitingDelete = new List<int>(); //等待删除列表
protected void Update()//更新
{
foreach (GameAction ac in waitingAdd)
{
actions[ac.GetInstanceID()] = ac; //设置映射关系 设置id映射到的动作
}
waitingAdd.Clear();//遍历了一遍等待加入列表 清空等待加入列表
foreach (KeyValuePair<int, GameAction> kv in actions)//遍历actions
{
GameAction ac = kv.Value;
if (ac.destroy)//如果已经销毁
{
waitingDelete.Add(ac.GetInstanceID());//id加入等待删除列表
}
else if (ac.enable)//否则进行更新
{
ac.Update();
}
}
foreach (int key in waitingDelete)//遍历等待删除列表
{
GameAction ac = actions[key];//从actions中删除
actions.Remove(key);
Destroy(ac);//销毁
}
waitingDelete.Clear();//遍历了一遍等待删除列表 清空等待删除列表
}
public void RunAction(GameObject gameobject, GameAction action, ISSActionCallback manager)//执行动作
{
action.gameobject = gameobject;//设置相应gameobject和transform
action.transform = gameobject.transform;
action.callback = manager;//设置回调者
waitingAdd.Add(action);//等待加入动作
action.Start();//执行
}
public void SSActionEvent(GameAction source, SSActionEventType events = SSActionEventType.Competeted,
int intParam = 0, string strParam = null, Object objectParam = null)
{
}
}
public class FlyActionManager : SSActionManager{//处理飞碟飞行的动作
public UFOFlyAction fly;//飞行动作
public Controllor scene;//场景控制器
protected void Start(){
scene = (Controllor)GameDirector.GetInstance ().CurrentSceneControllor;//获得当前场景控制器
scene.fam = this;
}
public void UFOfly(GameObject disk, float angle, float power){//飞行
fly = UFOFlyAction.GetSSAction(disk.GetComponent<DiskData> ().direction, angle, power);//根据参数设置水平方向 角度 初始速度
this.RunAction (disk, fly, this);//执行动作
}
}
public class UFOFlyAction : GameAction//实现了GameAction抽象类
{
public float gravity = -1.7f; //重力加速度
private Vector3 start_vector;//开始速度
private Vector3 gravity_vector = Vector3.zero; //向下速度
private float time; //时间
private Vector3 current_angle = Vector3.zero; //当前角度
private UFOFlyAction() { }
public static UFOFlyAction GetSSAction(Vector3 direction, float angle, float power)
{
UFOFlyAction action = CreateInstance<UFOFlyAction>();
if (direction.x == -1)//从右往左飞
{
action.start_vector = Quaternion.Euler(new Vector3(0, 0, -angle)) * Vector3.left * power;//计算开始速度
}
else//从左往右飞
{
action.start_vector = Quaternion.Euler(new Vector3(0, 0, angle)) * Vector3.right * power;//计算开始速度
}
return action;
}
public override void Update()//更新
{
time += Time.fixedDeltaTime;//增加相应时间
gravity_vector.y = gravity * time;//更新向下的速度
transform.position += (start_vector + gravity_vector) * Time.fixedDeltaTime;//更新位置
current_angle.z = Mathf.Atan((start_vector.y + gravity_vector.y) / start_vector.x) * Mathf.Rad2Deg;//更新角度
transform.eulerAngles = current_angle;//设置角度
if (this.transform.position.y < -10)//掉出范围外
{
this.destroy = true;//设置摧毁
this.callback.SSActionEvent(this);//回调管理者
}
}
public override void Start() { }
}
物理学动作控制器
物理学动作控制器同样继承了动作控制器接口,在逻辑上可以类比运动学动作控制器类进行实现。其使用场景控制器中的PhysicsUFOFlyAction类的函数,然后在PhysicsFlyActionManager 类中调用PhysicsUFOFlyAction类,但其不是像物理学动作控制器一样在Update中通过运动学公式计算出坐标位置,而是通过力等物理效果来实现对飞碟运动的操纵。具体来说,其首先在PhysicsUFOFlyAction类的Start方法中给定飞碟初始速度,并设置其受到重力作用。然后,物理引擎会根据动力学和运动学物理定律,计算出飞碟每一帧的位置。之后,只需要在PhysicsUFOFlyAction函数中判断飞碟是否飞出游戏范围,此时就和飞碟工厂进行配合回收飞碟。由于不再根据物理学定律手动计算和更新飞碟的位置,而是物理引擎自动完成飞碟位置的计算和更新,物理学动作控制器相比运动学动作控制器也就简单很多。代码如下,具体分析详见注释:
public class PhysicsFlyActionManager : SSActionManager
{//物理学动作控制器
public PhysicsUFOFlyAction fly; //飞行动作
protected void Start(){}
public void UFOfly(GameObject disk, float angle, float power)
{
fly = PhysicsUFOFlyAction.GetSSAction(disk.GetComponent<DiskData>().direction, angle, power);//根据参数获得对应飞行动作
this.RunAction(disk, fly, this);//执行该动作
}
}
public class PhysicsUFOFlyAction : SSAction
{
private Vector3 start_vector;
public float power;
private PhysicsUFOFlyAction() { }
public static PhysicsUFOFlyAction GetSSAction(Vector3 direction, float angle, float power) {//根据参数获得对应飞行动作
PhysicsUFOFlyAction action = CreateInstance<PhysicsUFOFlyAction>();//根据初始x坐标决定从左到右飞还是从右到左飞
if (direction.x == -1) action.start_vector = Quaternion.Euler(new Vector3(0, 0, -angle)) * Vector3.left * power;
else action.start_vector = Quaternion.Euler(new Vector3(0, 0, angle)) * Vector3.right * power;
action.power = power;//设置对应初始power
return action;//返回对应飞行动作
}
public override void FixedUpdate() {//更新
if (this.transform.position.y < -10) {//检查是否飞出范围
this.destroy = true;//销毁
this.callback.SSActionEvent(this);//回调动作事件
}
}
public override void Update() { }
public override void Start() {//初始化
gameobject.GetComponent<Rigidbody>().velocity = power / 40 * start_vector;//初始速度
gameobject.GetComponent<Rigidbody>().useGravity = true;//使用重力 物理引擎便可以自动维护飞碟在重力作用下的飞行位置
}
}
主场景控制器
主场景控制器具体实现了之前定义的接口中的函数。游戏开始时其通过isCounting()函数以及getEmitTime()函数判断开始倒计时是否完成,若倒计时结束则将counting(是否倒计时)设为false并开始发送飞碟。之后其在每一帧中,其以speed的时间间隔扔出飞碟,进入下一轮后就加速speed以更快的方式扔出飞碟来提高游戏难度。Hit函数实现了玩家通过点击发出子弹摧毁飞碟。玩家若击中飞碟则触发飞碟的粒子爆炸效果。GetScore(),GetRound(),Restart(),GameOver()等函数则都是与GUI交互的函数。同时其利用了定义的单例模板类,保证了飞碟工厂是单实例的。相比上一个版本的游戏,主场景控制器增加了游戏模式字段mode,同时也调整一些函数接口,不再是和动作控制器直接打交道,而是通过动作控制器适配器间接与动作控制器交互。代码如下,具体分析详见代码注释:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;
public class Controllor : MonoBehaviour,ISceneControllor,IUserAction
{
public IActionManager iam;//飞行动作管理者
public DiskFactory df;//飞碟工厂
public UserGUI ug;//用户GUI
public ScoreRecorder sr;//计分者
public RoundControllor rc;//轮控制器
private Queue<GameObject> dq = new Queue<GameObject> ();//飞碟队列
private List<GameObject> dfree = new List<GameObject> ();//空闲飞碟列表
private GameObject explosion;//爆炸效果
private float emit_time = 3;//倒计时时间
private int round = 1;//当前轮数
private float t = 1;//临时变量
private float speed = 2;//飞碟间隔
private bool game_over = false; //游戏是否结束
private bool counting = true;//是否在倒计时
public bool modeSetting = true;//是否在游戏模式选择界面
public bool isCounting(){return counting;}//是否在倒计时
public int getEmitTime(){return (int)emit_time+1;}//获得倒计时时间
public void Restart (){SceneManager.LoadScene(0);}//重新开始
public int GetScore (){return sr.score;}//获得分数
public int GetRound (){return round;}//获得轮数
public void modeSet (bool flag){mode = flag;}//设置游戏模式
public void gameBegin (){modeSetting = false;}//游戏开始
public bool getModeSetting (){return modeSetting;}//获得当前是否在游戏模式选择界面
public void GameOver (){game_over = true;}//游戏结束
public bool mode = false;//游戏模式
void Start(){
GameDirector director = GameDirector.GetInstance(); //创建导演
director.CurrentSceneControllor = this; //设置当前场景控制器
df = Singleton<DiskFactory>.Instance;//飞碟工厂
sr = gameObject.AddComponent<ScoreRecorder> () as ScoreRecorder;//计分者
iam = gameObject.AddComponent<ActionManagerAdapter>() as IActionManager;//飞行动作管理者
ug = gameObject.AddComponent<UserGUI>() as UserGUI;//用户GUI
rc = gameObject.AddComponent<RoundControllor> () as RoundControllor;//轮控制器
explosion = Instantiate (Resources.Load<GameObject> ("Prefabs/ParticleSystemG"), new Vector3(0, -100, 0), Quaternion.identity);//爆炸的粒子系统效果
t = speed;//飞碟间隔
}
void Update ()
{ if (modeSetting == false) {//已经进入了游戏
if (emit_time > 0) {//倒计时状态
counting = true;
emit_time -= Time.deltaTime;
} else {//非倒计时状态
counting = false;
t-=Time.deltaTime;
if (t < 0) {//间隔到了 扔飞碟
if (game_over == false) {
LoadResources ();
SendDisk ();
t = speed;
}
}
if (sr.number % 10 == 0) {//轮数增加的条件
round++;//轮数增加
rc.loadRoundData (round);//下一轮
}
}
}
}
public void setting(float speed_,GameObject explosion_)//设置速度和爆炸
{
speed = speed_;
explosion = explosion_;
}
public void LoadResources()//载入资源
{
dq.Enqueue(df.GetDisk(round));
}
private void SendDisk()//扔飞碟
{
float position_x = mode ? 16 : 11; //初始位置
if (dq.Count != 0)//队列数目不为0
{
GameObject disk = dq.Dequeue();//从队列中出来
dfree.Add(disk);//加入dfree列表
disk.SetActive(true);//激活
float ran_y = mode ? Random.Range(1f, 4f) : Random.Range(3f, 4f);//随机高度
float ran_x = Random.Range(-1f, 1f) < 0 ? -1 : 1;//随机方向(左往右还是右往左)
disk.GetComponent<DiskData>().direction = new Vector3(ran_x, ran_y, 0);//随机扔出的方向
Vector3 position = new Vector3(-disk.GetComponent<DiskData>().direction.x * position_x, ran_y, 0);//随机扔出的位置
disk.transform.position = position;
float power = mode ? Random.Range(20f, 30f) : Random.Range(3.5f, 5f);//随机初始速度
float angle = mode ? Random.Range(5f, 13f) : Random.Range(20f, 35f);//随机角度
iam.UFOFly(disk,angle,power,mode);//执行飞行
}
for (int i = 0; i < dfree.Count; i++)//遍历dfree中的飞碟
{
GameObject temp = dfree[i];
if (temp.transform.position.y < -10 && temp.gameObject.activeSelf == true)//检查是否越界
{
df.FreeDisk(dfree[i]);//回收
dfree.Remove(dfree[i]);
ug.ReduceBlood();//扣血
}
}
}
public void Hit (Vector3 pos){//击中飞碟
Ray ray = Camera.main.ScreenPointToRay(pos);//通过射线检查
RaycastHit[] hits;
hits = Physics.RaycastAll(ray);//获得击中的点
bool not_hit = false;
for (int i = 0; i < hits.Length; i++)
{
RaycastHit hit = hits[i];
if (hit.collider.gameObject.GetComponent<DiskData>() != null)//判断是否击中飞碟
{//击中了飞碟
for (int j = 0; j < dfree.Count; j++)
{
if (hit.collider.gameObject.GetInstanceID() == dfree[j].gameObject.GetInstanceID())//是dfree中的飞碟
{
not_hit = true;//击中了
}
}
if(!not_hit)//没有击中
{
return;
}
dfree.Remove(hit.collider.gameObject);//移除对应的飞碟
sr.Record(hit.collider.gameObject);//计分者记录
explosion.transform.position = hit.collider.gameObject.transform.position;//设置碎片飞溅效果
explosion.GetComponent<ParticleSystem>().Play();
hit.collider.gameObject.transform.position = new Vector3(0, -100, 0);
df.FreeDisk(hit.collider.gameObject);
break;
}
}
}
}
计分者
这个类的功能比较简单,其根据每次击中飞碟的不同加上不同的分数。而具体的分数在DiskData中进行了设置。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class ScoreRecorder : MonoBehaviour {
public int score = 0;
void Start(){
score = 0;
}
public void Record(GameObject disk){
score = score + disk.GetComponent<DiskData> ().score;
}
}
轮控制器
下面的轮控制器实现了5轮。每一轮的飞碟颜色和速度都不同。且随着轮数增大,飞碟产生速度越快,游戏难度也越大。这样就实现了游戏的需求。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class RoundControllor : MonoBehaviour {
private IUserAction action;
private float speed;
private GameObject explosion;
void Start(){
action = GameDirector.GetInstance().CurrentSceneControllor as IUserAction;
speed = 3f;
}
public void loadRoundData(int round)
{
switch (round)
{
case 1:
break;
case 2:
speed = 2.5f;
explosion = Instantiate (Resources.Load<GameObject> ("Prefabs/ParticleSystemYellow"), new Vector3(0, -100, 0), Quaternion.identity);
action.setting (speed,explosion);
break;
case 3:
speed = 2f;
explosion = Instantiate (Resources.Load<GameObject> ("Prefabs/ParticleSystemRed"), new Vector3(0, -100, 0), Quaternion.identity);
action.setting (speed,explosion);
break;
case 4:
speed = 1.5f;
explosion = Instantiate (Resources.Load<GameObject> ("Prefabs/ParticleSystemGreen"), new Vector3(0, -100, 0), Quaternion.identity);
action.setting (speed,explosion);
break;
case 5:
speed = 1f;
explosion = Instantiate (Resources.Load<GameObject> ("Prefabs/ParticleSystemYellow"), new Vector3(0, -100, 0), Quaternion.identity);
action.setting (speed,explosion);
}
}
}
导演类
这个类起到了导演的功能。其是整个游戏中最高的控制器,由初始场景的主控制器调用载入。导演类采用单体设计模式,在整个游戏过程中仅有一个实例对象。当前游戏中没有用到场景的切换,因此只是简单地设计了导演类对象,并没有在其中具体实现复杂的功能。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class GameDirector : System.Object
{
private static GameDirector _instance;
public ISceneControllor CurrentSceneControllor{ get;set;}
public static GameDirector GetInstance(){
if (_instance == null) {
_instance = new GameDirector();
}
return _instance;
}
}
部署脚本
可以使用项目链接中的场景,这样就不需要部署脚本。也可以自己创建一个新的场景,对于这个新的场景,只需要在游戏中创建一个除了摄像头和光源之外的空对象,然后将第一个场景主控制器的脚本Controller.cs(在这个游戏中也是唯一一个场景主控制器)和飞碟工厂脚本DiskFactory.cs拖动到这个空对象上,就可以完成脚本的部署。
调整摄像头
调整摄像头的位置和姿态,使得其可以以比较好的视角看见游戏场景。
这样就完成了项目的配置。
游戏效果截图
下面的游戏效果截图侧重于展示新设计的游戏和上一个游戏的不同点。
游戏开始时是游戏模式选择界面:

选择一种模式后,开始倒计时:

运动学模式
飞碟:

碎片飞溅:

游戏结束:

其他运动学模式的效果(及截图)和上一个游戏版本大同小异。
物理学模式
飞碟:

碎片飞溅:

如果进入了第二轮:

黄色飞碟打中的分数更高。黄色飞碟飞溅:

游戏结束:

重新开始,所有状态被正常初始化:

本文介绍了一个3D游戏项目的设计与实现过程,重点讨论了如何通过适配器模式整合运动学和物理学两种不同的动作控制方式,以提升游戏的可扩展性和可维护性。
670

被折叠的 条评论
为什么被折叠?



