目录
3. PhysicFlyAction飞碟动作类(物理刚体运动)
8. PhysicActionManager(物理刚体)动作管理类
前言
飞碟游戏是一款经典的射击类游戏,玩家需要通过射击飞碟来获取分数,游戏的目标是尽可能地获得高分。以下是飞碟游戏的一般规则和玩法:
1. 游戏目标:玩家通过射击飞碟来获得分数,目标是尽可能地获得高分。
2. 飞碟类型:游戏中通常会有多种不同类型的飞碟,每种飞碟的分数和速度可能不同,玩家需要根据情况选择目标并进行射击。在本游戏中,一共有三种飞碟的类型,分别对应着不同的颜色和不同的分数。
3. 射击操作:玩家可以使用鼠标进行射击,点击飞碟时,可以通过射线的方式来与飞碟产生碰撞,如果射线成功与飞碟产生碰撞,则得分增加。
4. 时间限制:游戏通常会有时间限制,玩家需要在规定的时间内尽可能地获得高分。
5. 关卡和难度:游戏10个关卡,每个关卡的难度逐渐增加,如飞碟的速度增加、出现更多的飞碟,可以增加游戏的挑战性和可玩性。
首先让我们看一下游戏的演示视频吧:
游戏对象及其预制体
![](https://img-blog.csdnimg.cn/5a1ec54abb3b4366b8b718bd4fab948d.png)
我们需要首先创建一下该游戏的预制体,由于该3D游戏是一个简单版本的打飞碟游戏,所以我们只需要一个游戏对象——飞碟即可,但是前面说过,我们需要有三个不同的飞碟类型,分别对应着不同的颜色和得分,我们不应该需要三个不同的飞碟预制体吗?是的,其实使用三个不同的飞碟预制体是没有问题的,但是我们也可以使用一个预制体即可,我们可以直接使用代码将三种不同的飞碟渲染上不同的颜色以及分配不同的分数即可。以上就是我们制作的一个飞碟预制体。具体的飞碟预制体过程请自行查阅百度。
天空盒的制作
- 下载天空盒相关项目,它会包含一些天空资源,例如:
- 使用 skybox 在 Store 中搜索
- 选择 Free Assets
- 选择 Fantasy Skybox FREE,然后 Add to my Assets
- 选择 Open in Unity
- 网页自动打开 Editor 的 Package manager
- 选择 Fantasy Skybox FREE 包,点 download 按钮
- 完成后 Import 导入。导入所有资源
- 最后,Fantasy Skybox FREE 目录出现在你的项目中
- CubeMaps(立方图和六面体贴图)
- Panoramics(全景图)
- Scenes(演示场景)
- 在导入素材后我们也可以自己定义天空盒材料
- Assets 上下文菜单 -> create -> Material 起名 mysky
- 在 Inspector 视图中选择 Shader 是上述三种之一,例如:skybox/6side
- 将对应纹理(texture)/图片拖入对应参数
- 要将天空盒分配给您正在处理的场景,请执行以下操作:
- 从菜单栏中选择 Window > Rendering > Lighting Settings
- 在随后出现的窗口中选择 Environment 选项卡
- 将新的天空盒材质拖放到 Skybox Material 字段
如图所示,在本游戏中,这是我所添加的天空盒,具体的天空盒设计完全可以由你自己来进行创造和设计,按照自己的想法进行设计即可。
对象池的原理以及对象配置方法
当需要频繁创建和销毁对象时,我们就可以使用对象池(Object Pool)来提高性能和资源利用率。对象池维护一组已经创建好的对象,而不是每次需要时都创建新的对象。下面我们将使用伪代码来讲解对象池的原理:
// 定义对象池类
class ObjectPool:
private pool: List<Object> // 对象池,用于存储可重复使用的对象
constructor(size: int):
pool = new List<Object>() // 初始化对象池为空列表
initializePool(size) // 初始化对象池,创建一定数量的对象
private initializePool(size: int):
for i in range(size):
object = new Object() // 创建新的对象
pool.add(object) // 将对象添加到对象池中
public getObject():
if pool.isEmpty():
object = new Object() // 如果对象池为空,则创建新的对象
else:
object = pool.removeLast() // 从对象池中获取一个可重复使用的对象
return object
public releaseObject(object: Object):
pool.add(object) // 将不再使用的对象放回对象池中
// 使用对象池
pool = new ObjectPool(10) // 创建一个包含10个对象的对象池
// 从对象池中获取对象
object1 = pool.getObject()
// 使用对象1进行操作
object2 = pool.getObject()
// 使用对象2进行操作
// 释放对象,将其放回对象池中
pool.releaseObject(object1)
// 再次从对象池中获取对象
object3 = pool.getObject()
// 使用对象3进行操作
我们可以知道以上伪代码就演示了对象池的基本原理:对象池在初始化时创建一定数量的对象,并将它们存储在一个列表中。当需要获取对象时,首先检查对象池是否为空。如果不为空,则从对象池中获取一个对象;如果为空,则创建一个新的对象。当使用完一个对象后,可以通过将其放回对象池中来释放它,以便其他地方可以重复使用。
适配器模式(Adapter Pattern)的原理
适配器模式(Adapter Pattern)是一种结构型设计模式,用于将一个类的接口转换成客户端所期望的另一个接口。下面是使用伪代码解释适配器模式的基本原理:
// 目标接口(Target Interface) - 客户端所期望的接口
interface TargetInterface:
methodA()
// 原始类(Adaptee) - 需要被适配的类或接口
class Adaptee:
methodB()
// 适配器(Adapter) - 将原始接口转换为目标接口的类
class Adapter implements TargetInterface:
private adaptee: Adaptee
constructor(adaptee: Adaptee):
this.adaptee = adaptee
methodA():
// 在目标接口的方法调用中调用原始类的相应方法
this.adaptee.methodB()
// 使用适配器模式
adaptee = new Adaptee() // 创建原始类对象
adapter = new Adapter(adaptee) // 创建适配器对象,并将原始类对象传递给适配器
adapter.methodA() // 调用适配器的目标接口方法,实际上会调用原始类的对应方法
在上述示例中,适配器模式的关键在于适配器(Adapter)实现了目标接口(TargetInterface),并持有一个原始类(Adaptee)的实例。适配器的 methodA() 方法实际上是在目标接口的方法调用中调用了原始类的 methodB() 方法,通过适配器将原始接口转换为目标接口。
通过适配器模式,客户端可以通过与目标接口进行交互,而无需直接与原始类进行交互。适配器在其中起到了桥梁的作用,使得原本由于接口不兼容而不能一起工作的类能够协同工作。
UML设计图
在本游戏中,我们的大体框架还是基于MVC架构的,同时沿用了上一个实验的方法,即是动作分离版本的MVC架构,将会创建控制器、动作分离以及用户交互界面等几个大的模块来实现。同时在这一个实验中,我们还需要注意的是,需要不断创建和销毁飞碟对象,所以我们需要使用工厂模式来管理飞碟对象。游戏由导演、场记、运动管理师、演员构成,而在游戏中,场记请了记分员、飞碟管理员DiskFactory,飞碟管理员管理飞碟的发放与回收,自己有个小仓库管理这些飞碟。而记分员主要按照飞碟的数据计分,记分员拥有计分规则。场记主控制器就只需要管理出飞碟规则与管理碰撞就可以了。
而在这一个实验中,我们还需要实现场景单实例,即是确保场记、飞碟管理员等均只有一个实例对象。同时,还需要采用适配器(Adapter)模式进行接口的转换,将原来只实现了动力学运动的接口改造可以与物理刚体运动接口兼容的接口实现。这样子设计既可以保留原来的接口实现,也可以继续增加新的不兼容的接口实现。这一个模式具有解耦性、可复用性以及拓展性,大大便于编程的实现以及设计。
代码实现
(一)Action动作和动作管理器
1. SSAction动作基类
动作基类(SSAction)作为一个抽象类,用于定义我们的游戏中各种动作的基本行为和属性。它继承自ScriptableObject,可以创建自定义的脚本化文件,用于在编辑器和运行时配置动作的属性。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
//动作基类
//使用ScriptableObject文件定制属性配置
//创建可在编辑器和运行中使用的自定义脚本化文件
public class SSAction : ScriptableObject
{
//表示是否启用该动作
public bool enable = true;
//表示是否销毁该动作
public bool destroy = false;
//用于获取和设置与该动作关联的游戏对象
public GameObject gameObject { get; set;}
//用于获取和设置与该动作关联的游戏对象的变换组件
public Transform transform {get; set;}
//用于获取和设置与该动作关联的回调函数
public IActionCallback callback {get; set;}
//无参构造函数,用于创建实例
protected SSAction() {}
// Start is called before the first frame update
//在子类中要求实现,如果没有实现,就抛出异常
public virtual void Start()
{
throw new System.NotImplementedException();
}
// Update is called once per frame
//在子类中要求实现,如果没有实现,就抛出异常
public virtual void Update()
{
throw new System.NotImplementedException();
}
}
2. CCFlyAction飞碟动作类(动力学运动)
飞碟的动力学运动有两个简单的属性:水平方向运动速度和垂直方向运动速度。
飞碟从飞碟工厂DiskFactory出来的时候被定位在相机的四个视角边缘,随着运动进入相机视角,在被玩家点击或者飞出相机视角(即玩家不能再看到它时)时,飞碟和动作一起被销毁(回收)。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
//飞碟动作类(运动学)
//继承自动作基类
//飞碟从界面左右两侧飞入,离开界面时运动结束
//飞碟的运动有两个方向,主要为水平运动方向和垂直运动方向
public class CCFlyAction : SSAction
{
//在水平方向上的运动速度
public float speedX;
//在垂直方向上的运动速度
public float speedY;
//用于创建并返回飞碟运动的实例
//接收飞碟在水平和垂直方向上的速度并且设置速度
//返回该飞碟运动的动作
public static CCFlyAction GetSSAction(float x, float y) {
CCFlyAction action = ScriptableObject.CreateInstance<CCFlyAction>();
action.speedX = x;
action.speedY = y;
return action;
}
// Start is called before the first frame update
public override void Start()
{
//将刚体的是否为运动学运动的属性变量设置为True
gameObject.GetComponent<Rigidbody>().isKinematic = true;
}
// Update is called once per frame
public override void Update()
{
//首先检查飞碟游戏对象是否处于非激活状态(被销毁)
if (this.transform.gameObject.activeSelf == false) {
//将销毁destroy设置为True
this.destroy = true;
//通过回调函数接口通知事件发生
this.callback.SSActionEvent(this);
//随后返回
return;
}
//将飞碟的世界坐标转换为屏幕坐标
Vector3 vec3 = Camera.main.WorldToScreenPoint (this.transform.position);
//如果飞碟已经超出了屏幕的范围,就将destroy设置为True,同时通过回调函数接口通知事件发生,同时返回
if (vec3.x < -100 || vec3.x > Camera.main.pixelWidth + 100 || vec3.y < -100 || vec3.y > Camera.main.pixelHeight + 100) {
this.destroy = true;
this.callback.SSActionEvent(this);
return;
}
//如果飞碟还在屏幕的范围内,就通过在水平和垂直的速度更新飞碟的速度状态
transform.position += new Vector3(speedX, speedY, 0) * Time.deltaTime * 2;
}
}
3. PhysicFlyAction飞碟动作类(物理刚体运动)
这一个动作类与CCFlyAction同样继承自SSAction,但是由于添加了刚体属性,即该物体本身就有了重力,现在只需要一个水平方向的初速度即可实现物体的运动了。我们可以在Update函数里看到两种运动的明显差别,CCFlyAction在Update函数中通过position.translate来不断更新改变物体的位置,而PhysicFlyAction什么都不用做,物体自身带有的物理属性(重力和初速度)就会使它运动起来。而为了区分这一个物体的运动究竟是属于动力学运动还是物理刚体运动,我们还需要添加isKinematic变量来进行标记,需要注意的是做刚体物理运动时要将isKinematic设为false,而作动力学运动时需要将其设为true。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
//飞碟动作类(物理刚体运动)
//只需要水平方向上的初速度,在垂直方向上考虑重力
//飞碟从界面左右两侧飞入,离开界面时运动结束
public class PhysicFlyAction : SSAction
{
//只需要定义一个变量用于1存储水平方向上的运动速度
public float speedX;
//用于创建一个PhysicFlyAction实例,并且设置其速度
//返回创建的该实例
public static PhysicFlyAction GetSSAction(float x) {
PhysicFlyAction action = ScriptableObject.CreateInstance<PhysicFlyAction>();
action.speedX = x;
return action;
}
// Start is called before the first frame update
public override void Start()
{
//将飞碟的刚体的isKinematic设置为了False,表示这是一个刚体的物理运动
gameObject.GetComponent<Rigidbody>().isKinematic = false;
//设置飞碟的初始速度
gameObject.GetComponent<Rigidbody>().velocity = new Vector3(speedX * 10, 0, 0);
//将飞碟的阻力设置为1,以模拟飞碟在空气中受到的空气阻力
gameObject.GetComponent<Rigidbody>().drag = 1;
}
//与运动学的动作不同,这里不需要不断更行飞碟的位置
//只需要给飞碟一个初速度以及重力,飞碟自己就会运动
// Update is called once per frame
public override void Update()
{
//检查飞碟是否处于非激活状态(被销毁)
if (this.transform.gameObject.activeSelf == false) {
//如果是,将destroy设置为True
this.destroy = true;
//调用回调接口的方法通知动作已经结束
this.callback.SSActionEvent(this);
//然后返回
return;
}
//将飞碟的世界坐标转换为屏幕坐标
Vector3 vec3 = Camera.main.WorldToScreenPoint (this.transform.position);
//判断飞碟是否超出了屏幕的范围
if (vec3.x < -100 || vec3.x > Camera.main.pixelWidth + 100 || vec3.y < -100 || vec3.y > Camera.main.pixelHeight + 100) {
//如果超出,将destroy设置为True
this.destroy = true;
//调用回调接口的方法通知该动作已经结束
this.callback.SSActionEvent(this);
//然后返回
return;
}
}
}
4. IActionCallback 事件回调接口
可以接收事件通知,该回调函数允许其他对象或系统向特定的对象发送事件通知。当某个事件发生时,可以调用回调函数,将事件信息传递给回调函数,以便对象能够作出相应的响应。回调函数还可以实现事件处理逻辑、实现定制化行为以及实现对象间的通信等。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
//定义了一个枚举类型,主要有两个状态,分别表示动作事件的开始和完成
public enum SSActionEventType:int {Started, Completed}
//定义一个接口,用于接收动作事件的回调函数
public interface IActionCallback
{
//回调函数
//source是触发动作事件的SSAction对象
//events是事件类型(开始和完成)
void SSActionEvent(SSAction source,
SSActionEventType events = SSActionEventType.Completed,
int intParam = 0,
string strParam = null,
Object objectParam = null);
}
5. IActionManager动作管理接口
这个接口类(IActionManager)提供了一个最简单的接口给主控制器调用,用于控制飞碟的飞行动作和获取当前回合飞碟的剩余数量。主要运用了Adapter模式,将原来的运动学动作管理类和物理刚体动作管理类进行了合并以及整理。通过这个接口,主控制器可以调用动作管理器来控制飞碟的飞行行为,并获取飞碟的状态信息。这样可以实现飞碟游戏的逻辑控制和状态管理。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
//提供一个最简单的接口给主控制器调用
public interface IActionManager
{
//控制飞碟的飞行动作
void Fly(GameObject disk);
//用于返回当前回合飞碟的剩余数
int RemainActionCount() ;
}
6. SSActionManager 动作管理类基类
该动作管理类基类(SSActionManager)用于管理和调度游戏中的各种动作。它是一个抽象类,提供了动作管理的基本框架和方法,可以被具体的动作管理器子类继承和扩展。提供了一个通用的动作管理框架,用于集中管理和调度游戏中的各种动作。它简化了动作的管理和更新过程,使得游戏开发者可以更方便地处理和控制动作的执行。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
//动作管理类基类
public class SSActionManager : MonoBehaviour
{
//定义一个字典,用于存储正在执行的动作,以动作实例的唯一标识符作为键
public Dictionary<int, SSAction> actions = new Dictionary<int, SSAction>();
//定义一个列表,用于存储等待添加到aactions字典中的动作实例
private List<SSAction> waitingAdd = new List<SSAction>();
//定义一个列表,用于存储等待从actions字典中删除动作实例的唯一标识符
private List<int> waitingDelete = new List<int>();
// Start is called before the first frame update
protected void Start(){}
// Update is called once per frame
protected void Update()
{
//首先将列表中等待添加到actions字典中的动作实例添加到actions字典中
foreach (SSAction ac in waitingAdd) {
actions[ac.GetInstanceID()] = ac;
}
//清空waitingAdd列表
waitingAdd.Clear();
//遍历在actions字典中的动作实例
foreach(KeyValuePair<int, SSAction> kv in actions) {
SSAction ac = kv.Value;
//如果动作实例中的destroy为True,将该动作实例添加到等待删除waitingDelete的列表中
if (ac.destroy) {
waitingDelete.Add(ac.GetInstanceID());
}
//如果动作实例的enable为True,就调用其Update方法进行更新
else if (ac.enable) {
ac.Update();
}
}
//根据waitingDelete列表中的唯一标识符,从actions字典中删除相对应的动作实例
foreach(int key in waitingDelete) {
SSAction ac = actions[key];
actions.Remove(key);
Destroy(ac);
}
//清空waitingDelete列表
waitingDelete.Clear();
}
//定义一个方法,用于运行一个动作
//接收一个游戏对象,一个动作实例,以及一个动作回调接口
public void RunAction(GameObject gameObject, SSAction action, IActionCallback manager) {
//为动作实例设置游戏对象和变换属性
action.gameObject = gameObject;
action.transform = gameObject.transform;
action.callback = manager;
//将动作实例添加到waitingAdd列表中
waitingAdd.Add(action);
//调用动作实例的start方法
action.Start();
}
//定义一个虚函数,用于移动飞碟的行为
//通过子类重写来实现特定的移动行为
public virtual void MoveDisk(GameObject disk){}
}
7. CCActionManager (运动学)动作管理类
该动作管理类主要继承于SSActionManager、IActionCallback和IActionManager,主要用于管理和控制飞碟的运动学运动行为,与场景控制器进行交互,并提供方法来控制飞碟的飞行和获取飞行中的飞碟数量。通过该类的实现,可以实现飞碟的创建、飞行和回收等动作的管理和控制。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
//(运动学)动作管理类
public class CCActionManager : SSActionManager, IActionCallback, IActionManager
{
//定义一个控制器变量,用于获取当前场景控制器
public RoundController sceneController;
//用于存储飞碟的飞行动作(运动学)
public CCFlyAction action;
//用于获取飞碟的工厂实例
public DiskFactory factory;
// Start is called before the first frame update
protected new void Start()
{
//获取当前场景控制器,并且调用导演类中的函数来确保场景的单实例
sceneController = (RoundController)SSDirector.getInstance().currentSceneController;
//将当前的CCActionManager设置为场景控制器的动作管理器
sceneController.actionManager = this as IActionManager;
//获取飞碟的工厂实例,确保工厂的单实例
factory = Singleton<DiskFactory>.Instance;
}
//重写回调函数
public void SSActionEvent(SSAction source,
SSActionEventType events = SSActionEventType.Completed,
int intParam = 0,
string strParam = null,
Object objectParam = null) {
//通过访问source的transform属性获取飞碟的游戏对象,并调用飞碟工厂的FreeDisk方法将飞碟回收
factory.FreeDisk(source.transform.gameObject);
}
public override void MoveDisk(GameObject disk) {
//根据飞碟的速度创建一个CCFlyAction动作实例
action = CCFlyAction.GetSSAction(disk.GetComponent<DiskAttributes>().speedX, disk.GetComponent<DiskAttributes>().speedY);
//将该动作实例作为参数传递给基类的RunAction方法,
//同时传递当前的CCActionManager实例作为动作的回调接口
RunAction(disk, action, this);
}
//这是IActionManager中的Fly函数,通过调用MoveDisk来控制飞碟的飞行
public void Fly(GameObject disk) {
MoveDisk(disk);
}
//返回当前正在(尚未)执行的飞碟的数量
public int RemainActionCount() {
return actions.Count;
}
}
8. PhysicActionManager(物理刚体)动作管理类
该动作管理类(PhysicActionManager)继承自基类 SSActionManager,并实现了接口 IActionCallback 和 IActionManager。该类使用物理刚体(Rigidbody)来实现飞碟的运动行为,与前面的运动学动作管理类有所不同,但是其它的各个方面还是基本上一致的。主要提供方法来控制飞碟的飞行和获取飞行中的飞碟数量,通过该类的实现,可以实现飞碟的创建、飞行和回收等动作的管理和控制,并与场景控制器进行交互。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
//(物理刚体)动作管理器
//体现Adapter模式,一个简单接口实现多个功能
public class PhysicActionManager : SSActionManager, IActionCallback, IActionManager
{
//定义一个控制器变量,用于获取当前场景控制器
public RoundController sceneController;
//用于存储飞碟的飞行动作(刚体物理)
public PhysicFlyAction action;
//用于获取飞碟的工厂实例,用于创建和管理飞碟
public DiskFactory factory;
// Start is called before the first frame update
protected new void Start()
{
//获取当前场景控制器,并且调用导演类中的函数来确保场景的单实例
sceneController = (RoundController)SSDirector.getInstance().currentSceneController;
//将当前的CCActionManager设置为场景控制器的动作管理器
sceneController.actionManager = this as IActionManager;
//获取飞碟的工厂实例,确保工厂的单实例
factory = Singleton<DiskFactory>.Instance;
}
//Update is called once per frame
protected new void Update(){}
public void SSActionEvent(SSAction source,
SSActionEventType events = SSActionEventType.Completed,
int intParam = 0,
string strParam = null,
Object objectParam = null) {
//通过访问source的transform属性获取飞碟的游戏对象,并调用飞碟工厂的FreeDisk方法将飞碟回收
factory.FreeDisk(source.transform.gameObject);
}
public override void MoveDisk(GameObject disk) {
//首先获取飞碟的水平速度,并且创建一个PhysicFlyAction实例对象,并传递给该方法速度
action = PhysicFlyAction.GetSSAction(disk.GetComponent<DiskAttributes>().speedX);
//将该动作实例作为参数传递给基类的RunAction方法,
//同时传递当前的CCActionManager实例作为动作的回调接口
RunAction(disk, action, this);
}
//这是IActionManager中的Fly函数,通过调用MoveDisk来控制飞碟的飞行
public void Fly(GameObject disk) {
MoveDisk(disk);
}
//返回当前正在(尚未)执行的飞碟的数量
public int RemainActionCount() {
return actions.Count;
}
}
(二)Controllers控制器
1. DiskFactory飞碟生成器
这个飞碟生成器(DiskFactory)的作用是创建和管理飞碟游戏对象。它使用了工厂模式,通过利用对象池来对飞碟进行管理、创建和销毁操作。
GetDisk(int round)被主控制器调用,round(回合数)会影响所生产的飞碟的速度、大小等属性。
有两个列表used和free,存放的是飞碟属性(包括分数、速度),可以循环使用,提高飞碟的产生效率。还需要初始飞碟位置随机,可能为屏幕的四个角落之一,需要根据飞碟的分数和回合数设置飞碟的大小和速度。该飞碟生成器可以根据不同的回合数和属性设置生成不同属性的飞碟,并在飞碟完成任务后将其回收到未使用的列表中。这种设计模式可以提高游戏对象的重复使用性和性能效率。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
//自定义异常类
public class MyException : System.Exception
{
//无参构造函数
public MyException() {}
//带异常信息的有参构造函数
public MyException(string message) : base(message) {}
}
//用于存储飞碟的属性信息
public class DiskAttributes : MonoBehaviour
{
//public GameObject gameobj;
//分数
public int score;
//飞碟的水平方向上的速度
public float speedX;
//飞碟的垂直方向上的速度
public float speedY;
}
//飞碟生成器
//工厂模式
//用于创建和管理飞碟
public class DiskFactory : MonoBehaviour
{
//用于存储已使用的飞碟游戏对象的列表
List<GameObject> used;
//用于存储非使用的飞碟游戏对象的列表
List<GameObject> free;
//用于生成随机数
System.Random rand;
// Start is called before the first frame update
void Start()
{
//进行了初始化操作
//将used和free实例化
//并且创建了一个rand的实例
used = new List<GameObject>();
free = new List<GameObject>();
rand = new System.Random();
}
// Update is called once per frame
void Update(){}
//用于获取一个飞碟游戏对象
public GameObject GetDisk(int round) {
GameObject disk;
//首先检查未使用的飞碟列表free是否为空
//如果不为空,就从列表中获取第一个飞碟
//并且将这个飞碟从free列表中移除
if (free.Count != 0) {
disk = free[0];
free.Remove(disk);
}
else {
//如果free列表为空,就从资源列表中获取一个飞碟预制体
disk = GameObject.Instantiate(Resources.Load("Prefabs/disk", typeof(GameObject))) as GameObject;
//添加飞碟属性组件
disk.AddComponent<DiskAttributes>();
//添加刚体组件
disk.AddComponent<Rigidbody>();
}
//根据不同round设置diskAttributes的值
//为飞碟的角度设置一个随意的欧拉角
//其中X轴的旋转角度在-20到-40之间
disk.transform.localEulerAngles = new Vector3(-rand.Next(20,40),0,0);
//获取飞碟对象上的飞碟属性组件
DiskAttributes attri = disk.GetComponent<DiskAttributes>();
//设置该飞碟属性组件的分数属性为1到3之间
attri.score = rand.Next(1,4);
//由分数来决定速度、颜色、大小
attri.speedX = (rand.Next(1,5) + attri.score + round) * 0.2f;
attri.speedY = (rand.Next(1,5) + attri.score + round) * 0.2f;
//如果飞碟的分数为3,就将该飞碟的颜色设置为红色,并且稍微缩小一定的比例
if (attri.score == 3) {
disk.GetComponent<Renderer>().material.color = Color.red;
disk.transform.localScale += new Vector3(-0.5f,0,-0.5f);
}
//如果飞碟的分数为2,就将该飞碟的颜色设置为绿色,并且稍微缩小一定的比例
else if (attri.score == 2) {
disk.GetComponent<Renderer>().material.color = Color.green;
disk.transform.localScale += new Vector3(-0.2f,0,-0.2f);
}
//如果飞碟的分数为1,就将该飞碟的颜色设置为蓝色
else if (attri.score == 1) {
disk.GetComponent<Renderer>().material.color = Color.blue;
}
//飞碟可从四个方向飞入(左上、左下、右上、右下)
//随机生成一个随机数,用于确定飞碟从哪一个方向飞入
int direction = rand.Next(1,5);
//根据前面生成的随机数确定飞碟的飞入方向,根据不同的方向
//使用disk.transform.Translate将飞碟的初始位置设置在屏幕的不同边缘
//同时,根据飞碟的不同方向对飞碟的速度进行适当的调整
//使得飞碟在X轴和Y轴上反向移动
if (direction == 1) {
disk.transform.Translate(Camera.main.ScreenToWorldPoint(new Vector3(0, Camera.main.pixelHeight * 1.5f, 8)));
attri.speedY *= -1;
}
else if (direction == 2) {
disk.transform.Translate(Camera.main.ScreenToWorldPoint(new Vector3(0, Camera.main.pixelHeight * 0f, 8)));
}
else if (direction == 3) {
disk.transform.Translate(Camera.main.ScreenToWorldPoint(new Vector3(Camera.main.pixelWidth, Camera.main.pixelHeight * 1.5f, 8)));
attri.speedX *= -1;
attri.speedY *= -1;
}
else if (direction == 4) {
disk.transform.Translate(Camera.main.ScreenToWorldPoint(new Vector3(Camera.main.pixelWidth, Camera.main.pixelHeight * 0f, 8)));
attri.speedX *= -1;
}
//将生成的飞碟添加到已使用的飞碟列表中
used.Add(disk);
//将飞碟设置为激活状态
disk.SetActive(true);
//输出日志信息
Debug.Log("generate disk");
//返回该飞碟实例
return disk;
}
//用于释放一个飞碟游戏对象
public void FreeDisk(GameObject disk) {
//将飞碟的激活状态设置为False
disk.SetActive(false);
//将位置和大小恢复到预制,这点很重要!
disk.transform.position = new Vector3(0, 0,0);
disk.transform.localScale = new Vector3(2f,0.1f,2f);
//检查是否在已使用的飞碟列表used中包含该飞碟对象
if (!used.Contains(disk)) {
//如果不包含,就抛出自定义异常
throw new MyException("Try to remove a item from a list which doesn't contain it.");
}
//输出日志信息
Debug.Log("free disk");
//将飞碟从已使用的飞碟列表中移除
used.Remove(disk);
//将该飞碟对象添加到free对象列表中
free.Add(disk);
}
}
2. ScoreController 分数控制器
这个分数控制器(ScoreController)的作用就是记录游戏中的得分并与用户交互类交互然后将得分显示在用户界面上。它与场景中的其他组件(如 RoundController 和 UserGUI)进行交互,以实现游戏得分的管理和显示功能。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
//分数控制器
//用于记录游戏中的得分
public class ScoreController : MonoBehaviour
{
//用于存储游戏得分
int score;
//用于引用当前场景的RoundController控制器
public RoundController roundController;
//用于引用当前场景的UserGUI组件
public UserGUI userGUI;
// Start is called before the first frame update
void Start()
{
//首先通过SSDirector.getInstance().currentSceneController获取当前场景的控制器
//并且将其转换为RoundController的类型
//同时将结果赋值给roundController
roundController = (RoundController)SSDirector.getInstance().currentSceneController;
//将当前的ScoreController实例赋值给roundController的scoreController属性
roundController.scoreController = this;
//通过GetComponent<UserGUI>()方法获取当前游戏对象上的UserGUI组件,并将结果赋值给userGUI
userGUI = this.gameObject.GetComponent<UserGUI>();
}
//用于记录得分
public void Record(GameObject disk) {
//通过disk.GetComponent<DiskAttributes>().score获取disk游戏对象上的DiskAttributes组件,并获取其score属性的值,
//然后将其累加到score变量上
score += disk.GetComponent<DiskAttributes>().score;
//接着,将累加后的score赋值给userGUI的score属性
userGUI.score = score;
}
}
3. Singleton 单实例代码
这个控制器(Singleton<T>)是一个泛型类,用于实现单例模式。它可以用于任何类型 T 的类,确保在整个游戏运行期间只有一个实例存在。它可以通过获取单例对象来提供统一的访问点,方便其他组件或类与该对象进行交互和共享数据。在 Unity 中,单例模式常用于管理全局的游戏状态、资源、配置等。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
//用于单实例化飞碟工厂
//<T>表示该类是一个泛型类,可以用任何类型来实例化
public class Singleton<T> : MonoBehaviour where T : MonoBehaviour
{
//声明一个静态的受保护的字段
//用于存储实例化的对象
protected static T instance;
//公共的只读属性
//通过该属性获取单例对象
public static T Instance {
get {
//首先检查instance是否为空
if (instance == null) {
//如果为空,就使用FindObjectOfType查找场景中第一个符合类型T的对象
//并且将其赋值给instance
instance = (T)FindObjectOfType (typeof(T));
//如果instance仍然为空,就输出错误日志,提示需要往场景中添加类型T的实例
if (instance == null) {
Debug.LogError ("An instance of " + typeof(T) +
" is needed in the scene, but there is none.");
}
}
//返回唯一的单实例对象
return instance;
}
}
}
4. ISceneController 场景控制器接口
通过定义这个接口,可以实现不同场景控制器之间的统一管理和交互。在 Unity 中,有多个场景控制器可能需要实现这个接口,例如游戏关卡控制器、菜单控制器等。通过实现相同的接口,可以使这些控制器具备相似的功能,并且可以通过接口类型进行统一的调用和管理。同时这个场景控制器接口定义了场景控制器的公共功能和方法,使得不同的场景控制器可以具备相似的接口,并实现各自的场景逻辑和功能。这样可以实现场景控制器之间的统一管理和交互。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
//场景控制器接口
public interface ISceneController
{
//用于加载资源的方法
void LoadSource();
//用于处理击中事件的方法
void GetHit();
}
5. SSDirector 导演类
通过这个类,可以实现全局的导演类,用于管理游戏的整体流程和场景切换。它可以作为一个中心控制器,负责协调和调度不同场景之间的切换和逻辑处理。其他场景控制器可以通过该类的实例(通过 SSDirector.getInstance() 获取)来获取当前的场景控制器,并进行统一的管理和交互。同时这个导演类用于实现游戏的整体控制和场景切换,通过单例模式确保只有一个实例存在,并提供接口(currentSceneController)来管理和切换不同的场景控制器。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
//导演类
public class SSDirector : System.Object
{
//用于声明一个私有的静态字段_instance,用于存储SSDirector的唯一实例
private static SSDirector _instance;
//定义了一个公共的属性currentSceneController
//用于获取或设置当前场景控制器,该属性的类型为ISceneController接口
public ISceneController currentSceneController {get; set;}
//用于获取SSDirector的实例
public static SSDirector getInstance() {
//首先检查_instance是否为空
if (_instance == null) {
//如果为空,就创建一个新的SSDirector实例
//并且将该实例赋值给_instance
_instance = new SSDirector();
}
//最后返回_instance
return _instance;
}
}
6. RoundController 主控制器
这个主控制器(RoundController)是游戏的核心控制类,负责连接用户与游戏,实现场景控制器的接口和用户操作的接口,它还负责管理游戏的回合控制、动作管理、得分记录和界面显示等核心逻辑。该控制器的Update的主要工作:在每个回合中从工厂获取飞碟,为飞碟绑定动作,令其开始运动。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.Threading;
//主控制器
//连接用户与游戏,分别需要实现场景控制器的接口和用户操作的接口
public class RoundController : MonoBehaviour, ISceneController, IUserAction
{
//表示当前游戏的回合数
int round = 0;
//表示游戏的最大回合数
int max_round = 10;
//表示每回合的计时器
float timer = 0.5f;
//游戏对象,表示飞碟游戏对象
GameObject disk;
//飞碟工厂类型,用于创建飞碟对象
DiskFactory factory ;
//实现了IActionManager接口的变量,用于处理飞碟对象的动作(运动学和刚体物理)
public IActionManager actionManager;
//用于管理得分
public ScoreController scoreController;
//用于管理用户界面
public UserGUI userGUI;
// Start is called before the first frame update
void Start(){}
// Update is called once per frame
void Update()
{
//首先检查userGUI的模式是否为0,如果是就直接返回
if (userGUI.mode == 0) return;
//接着根据userGUI的isKinematic属性选择合适的actionManager
//如果isKinematic是false,表明对象的动作为物理刚体运动,就需要使用PhysicActionManager
if (userGUI.isKinematic == false) {
actionManager = gameObject.GetComponent<PhysicActionManager>() as IActionManager;
}
//如果为True,就表明对象的动作为运动学动作,就需要使用CCActionManager
else {
actionManager = gameObject.GetComponent<CCActionManager>() as IActionManager;
}
//调用GetHit()方法处理玩家的点击操作
GetHit();
//调用gameOver()方法检查游戏是否结束
gameOver();
//如果当前回合数大于最大回合数,就直接返回,不再执行后续的操作
if (round > max_round) {
return;
}
//计时器递减
timer -= Time.deltaTime;
//判断计时器是否小于等于0并且actionManager的剩余动作是否为0
if (timer <= 0 && actionManager.RemainActionCount() == 0) {
//如果满足条件,就从工厂中得到10个飞碟,为其加上动作
for (int i = 0; i < 10; ++i) {
disk = factory.GetDisk(round);
//使用Fly函数为飞碟添加动作
actionManager.Fly(disk);
}
//回合数加1
round += 1;
//如果回合数小于等于最大回合数
//将回合数更新到userGUI的round上
if (round <= max_round) {
userGUI.round = round;
}
//将timer重置到4.0f
timer = 4.0f;
}
}
//Awake函数,该方法在脚本被唤醒时执行一次
void Awake() {
//首先通过SSDirector.getInstance()获取SSDirector的实例
SSDirector director = SSDirector.getInstance();
//并将当前场景的控制器设置为RoundController
director.currentSceneController = this;
//调用LoadSource()方法加载资源
director.currentSceneController.LoadSource();
//通过gameObject.AddComponent<T>()方法向游戏对象添加多个组件
//依次为UserGUI、PhysicActionManager、CCActionManager、ScoreController和DiskFactory
gameObject.AddComponent<UserGUI>();
gameObject.AddComponent<PhysicActionManager>();
gameObject.AddComponent<CCActionManager>();
gameObject.AddComponent<ScoreController>();
gameObject.AddComponent<DiskFactory>();
//通过Singleton<DiskFactory>.Instance 获取DiskFactory的单实例
//将获取到的单实例赋值给factory
factory = Singleton<DiskFactory>.Instance;
//通过gameObject.GetComponent<UserGUI>()获取当前游戏对象上的UserGUI组件
//将结果赋值给当前的userGUI
userGUI = gameObject.GetComponent<UserGUI>();
}
public void LoadSource() {}
//用于判断游戏是否结束
public void gameOver()
{
//判断当前回合数大于最大回合数并且actionManager的剩余动作数量为0
//将userGUI的gameMessage设置为"Game Over!",表示游戏结束
if (round > max_round && actionManager.RemainActionCount() == 0)
userGUI.gameMessage = "Game Over!";
}
//用于处理玩家的点击操作
public void GetHit() {
//首先通过Input.GetButtonDown("Fire1")来判断玩家是否按下鼠标左键
if (Input.GetButtonDown("Fire1")) {
//如果是,就创建一条射线,其原点为摄像机,方向为鼠标点击位置
Camera ca = Camera.main;
Ray ray = ca.ScreenPointToRay(Input.mousePosition);
//通过Physics.Raycast(ray, out hit)返回射线与场景物体的碰撞结果
RaycastHit hit;
if (Physics.Raycast(ray, out hit)) {
//如果射线与物体发生碰撞,则调用scoreController.Record()方法记录得分
//将碰撞到的物体设置为非活动状态,即隐藏该物体
scoreController.Record(hit.transform.gameObject);
hit.transform.gameObject.SetActive(false);
}
}
}
}
(三)Views 用户接口与用户GUI
1. IUserAction 用户接口
该用户接口(IUserAction)定义了一些逻辑接口函数,用于处理游戏结束和玩家点击的操作。通过定义用户接口(IUserAction),可以将游戏逻辑与用户操作解耦,使得游戏的逻辑和用户操作的处理可以分开实现,提高代码的可维护性和可扩展性。同时,该接口定义了游戏结束和玩家点击的操作,方便开发者在游戏中根据需求进行相应的处理。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
//用户接口
public interface IUserAction {
//用于处理游戏结束的逻辑接口函数
void gameOver();
//用于处理玩家点击的逻辑接口函数
void GetHit();
}
2. UserGUI 用户界面
这个用户界面(UserGUI)用于实现游戏的界面显示和用户交互。它包含了一些变量和方法,用于记录和显示游戏的状态信息,并根据用户的交互进行相应的操作。它主要用于实现游戏的界面显示和用户交互,负责绘制主菜单界面和游戏开始界面,显示游戏的状态信息,响应用户的点击操作,并将用户的操作传递给主控制器(RoundController)进行游戏逻辑的处理。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
//用户界面
public class UserGUI : MonoBehaviour
{
//用于记录用户处于哪一个界面
public int mode;
//用于记录用户的总得分
public int score;
//用于记录游戏处于的游戏轮数
public int round;
//用于记录需要返回给用户的游戏描述
public string gameMessage;
//用于记录当前飞碟的动作为运动学还是物理刚体
public bool isKinematic;
//表示用户行为动作接口
private IUserAction action;
//用于定义自定义字体格式
public GUIStyle bigStyle, blackStyle, smallStyle;
//用于存储像素字体
public Font pixelFont;
//表示主菜单每一个按键的宽度和高度
private int menu_width = Screen.width / 5, menu_height = Screen.width / 10;
// Start is called before the first frame update
void Start()
{
//首先进行初始化,将动作初始化为运动学动作
isKinematic = true;
//将当前的用户界面初始化为0
mode = 0;
//将需要显示的信息初始化为空字符
gameMessage = "";
//获取SSDirector的实例,并将其当前场景控制器转换为IUserAction接口类型,然后赋值给action变量
action = SSDirector.getInstance().currentSceneController as IUserAction;
//大字体初始化
//创建一个名为bigStyle的新GUIStyle对象
bigStyle = new GUIStyle();
//设置bigStyle的文本颜色为白色
bigStyle.normal.textColor = Color.white;
//设置bigStyle的背景为 null
bigStyle.normal.background = null;
//设置bigStyle的字体大小为 50
bigStyle.fontSize = 50;
//设置bigStyle的对齐方式为居中
bigStyle.alignment=TextAnchor.MiddleCenter;
//类似地,对blackStyle进行了相应的初始化
//black
blackStyle = new GUIStyle();
blackStyle.normal.textColor = Color.black;
blackStyle.normal.background = null;
blackStyle.fontSize = 50;
blackStyle.alignment=TextAnchor.MiddleCenter;
//小字体初始化
//类似地,对smallStyle进行了相应的初始化
smallStyle = new GUIStyle();
smallStyle.normal.textColor = Color.white;
smallStyle.normal.background = null;
smallStyle.fontSize = 20;
smallStyle.alignment=TextAnchor.MiddleCenter;
}
// Update is called once per frame
void Update(){}
//是Unity的生命周期方法,在每个渲染帧之后被调用,用于绘制GUI元素
void OnGUI() {
GUI.skin.button.fontSize = 20;
//根据mode的值进行分支调用
switch(mode) {
case 0:
mainMenu();
break;
case 1:
GameStart();
break;
}
}
//主菜单界面
void mainMenu() {
//在指定位置绘制标签,显示文本为"Hit UFO",使用预定义的bigStyle样式
GUI.Label(new Rect(Screen.width / 2 - menu_width * 0.5f, Screen.height * 0.1f, menu_width, menu_height), "Hit UFO", bigStyle);
//在指定位置绘制按钮,显示文本为"Start",使用指定的位置和大小,返回一个布尔值表示按钮是否被点击
bool button = GUI.Button(new Rect(Screen.width / 2 - menu_width * 0.5f, Screen.height * 3 / 7, menu_width, menu_height), "Start");
//如果按钮被点击,就将mode设置为1,即会调用GameStart函数
if (button) {
mode = 1;
}
}
//游戏开始的界面
void GameStart() {
//创建三个label
//一个用于显示返回的游戏信息
GUI.Label(new Rect(300, 60, 50, 200), gameMessage, bigStyle);
//用于返回游戏的得分
GUI.Label(new Rect(0,0,100,50), "Score: " + score, smallStyle);
//用于返回游戏的轮数
GUI.Label(new Rect(560,0,100,50), "Round: " + round, smallStyle);
//在指定位置绘制按钮,显示文本为"Kinematic/Not Kinematic",使用指定的位置和大小,返回一个布尔值表示该按钮是否被点击
if (GUI.Button(new Rect(Screen.width / 2 - menu_width * 0.9f, 0, menu_width * 1.8f, menu_height), "Kinematic/Not Kinematic")) {
//如果该按钮被点击
//就将isKinematic设置为原来的负值
isKinematic = !isKinematic;
}
}
}
结语
至此、我们的unity简单3D游戏——打飞碟已经介绍完毕啦。在该实验中,我们成功应用了动作分离版本的架构,以及Adapter、工厂模式和对象池等设计模式,实现了一个打飞碟的Unity3D游戏。这些设计模式的应用使得游戏的代码结构更加清晰和可扩展,提高了游戏的性能和可维护性。同时,我们也深入理解了这些设计模式的原理和应用场景。通过这个实验,我主要锻炼了自己的编程能力,并提升了对游戏开发的理解和技能。在未来的游戏开发中,我还可以继续运用这些设计模式和架构思想,提高游戏的质量和开发效率。
整一个实验已经完成,但是我觉得有一些地方我们还是可以改进的。比如我们可以为飞碟添加旋转效果,给飞碟加上更复杂的轨迹,比如上下左右起伏,可以添加击中飞碟时的画面效果,比如飞碟破碎或爆炸,同时还可以将射线的轨迹画出来,动态显示击中飞碟的过程。
最后,让我们一起动起手来,实现一个属于我们自己的小游戏吧。
完整代码的Github地址:打飞碟github完整代码