本文是中山大学软件工程学院2020级3d游戏编程与设计的作业5
- 编写一个简单的鼠标打飞碟(Hit UFO)游戏
游戏内容要求:
- 游戏有n个 round,每个round 都包括10次trial;
- 每个 trial的飞碟的色彩、大小、发射位置、速度、角度、同时出现的个数都可能不同。它们由该round 的ruler 控制;
- 每个 trial 的飞碟有随机性,总体难度随 round 上升;
- 鼠标点中得分,得分规则按色彩、大小、速度不同计算,规则可自由设定。
- 游戏的要求:
- 使用带缓存的工厂模式管理不同飞碟的生产与回收,该工厂必须是场景单实例的!具体实现见参考资源 Singleton 模板类
- 尽可能使用前面 MVC 结构实现人机交互与游戏模型分离
本次简单打飞碟小游戏使用了工厂模式管理不同飞碟的生产与回收。
**工厂模式(Factory Pattern)**是 游戏开发中最常用的设计模式之一。该模式将实例化对象的代码提取出来,放到一个类中统一管理和维护,达到和主项目的依赖关系的解耦。从而提高项目的扩展和维护性。也因此,在工厂模式中,我们在创建对象时不会对用户暴露创建逻辑,并且是通过使用一个共同的接口来指向新创建的对象。
本次代码仍旧使用了MVC模式,同时进行了动作分离。
由于动作分离中动作基类等代码基本一致,本次编程题中,一部分代码使用了上次作业(牧师与魔鬼动作分离版本)中的代码,复用的代码分别为:SSAction()
,SSActionManager()
,CCSequenceAction()
三个类。
同时,Interface()
与SSDirector()
两个类的设计在这三次游戏编程中也是基本一致,此处不进行介绍。
以下为代码解读:
飞碟的动作类。控制飞碟的飞行。实现的逻辑为根据飞碟的位置决定飞碟的运动,当飞碟位置在一定范围内时进行我们所设计的模拟位移,超出一定范围之后就停止位移,等待工厂的回收。
public class FlyAction : SSAction
{
public float gravity = -1; //向下的加速度
private Vector3 start_vector; //初速度向量
private Vector3 gravity_vector = Vector3.zero; //加速度的向量,初始时为0
private Vector3 current_angle = Vector3.zero; //当前时间的欧拉角
private float time; //已经过去的时间
private FlyAction() { }
public static FlyAction GetSSAction(int lor, float angle, float power)
{
//初始化物体将要运动的初速度向量
FlyAction action = CreateInstance();
if (lor == -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()
{
//计算物体的向下的速度,v=at
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;
//如果物体y坐标小于-10,动作就做完了
if (this.transform.position.y < -10)
{
this.deleted = true;
this.callback.SSActionEvent(this);
}
}
public override void Start() { }
}
飞碟的动作管理类。通过调用FlyAction控制飞碟的运动。
public class FlyActionManager : SSActionManager
{
public FlyAction fly;
public FirstController scene_controller;
protected void Start()
{
scene_controller = (FirstController)SSDirector.GetInstance().CurrentSceneController;
scene_controller.action_manager = this;
}
public void DiskFly(GameObject disk, float angle, float power)
{
int loc = disk.transform.position.x < 0 ? 1 : -1;
fly = FlyAction.GetSSAction(loc, angle, power);
this.RunAction(disk, fly, this);
}
}
裁判类。由于游戏到达第三回合之后就直接停止,裁判类无需再判断游戏的终止条件,只是进行游戏分数的记录。
public class Judge : MonoBehaviour
{
private float score;
void Start()
{
score = 0;
}
public void Record(GameObject disk)
{
score += disk.GetComponent().score;
}
public float GetScore()
{
return score;
}
public void Reset()
{
score = 0;
}
}
场景单实例类。当所需的实例第一次被需要时,在场景内搜索该实例,下一次使用时不需要搜索直接返回。
public class Singleton : MonoBehaviour where T : MonoBehaviour {
protected static T instance;
public static T Instance {
get {
if (instance == null) {
instance = (T)FindObjectOfType(typeof(T));
if (instance == null) {
Debug.LogError("An instance of " + typeof(T)
+ " is needed in the scene, but there is none.");
}
}
return instance;
}
}
}
负责游戏第一个场景的搭载。
在FirstController之中还编写了获取相应数值的GetXXX()
,重新开始游戏的实现游戏的ReStart()
,游戏结束的GameOver()
等小部件函数。
而更为重要的函数为Update()
、Hit()
、SendDisk()
三个函数。
Update()
检测每一帧的鼠标点击是否命中目标,同时根据此时的回合数设置不同的游戏难度。此处变量count用来控制飞碟的发射速度以及游戏回合切换的暂停时间:每经过一帧count加1,当count到达相应数值就发射飞碟,当回合结束时,使count在一定数值的帧内不再增加(此处引入变量hold来进行此操作),即可暂停发射飞碟并在此时插入切换回合的UI。
Hit()
检测射线与飞碟是否碰撞,如碰撞则计分并回收飞碟。
SendDisk()
从工厂中拿飞碟并根据种类设置一定范围内的随机发射参数,然后调用动作管理器执行发射飞碟的动作。
public class FirstController : MonoBehaviour, ISceneController, IUserAction
{
public FlyActionManager action_manager;
public DiskFactory disk_factory;
public UserGUI user_gui;
public Judge judge;
private int round = 1;
private int trial = 0;
private bool running = false;
private int count = 0;
public bool stop = false;
public int hold = 0;
void Start()
{
SSDirector director = SSDirector.GetInstance();
director.CurrentSceneController = this;
disk_factory = Singleton.Instance;
judge = Singleton.Instance;
action_manager = gameObject.AddComponent() as FlyActionManager;
user_gui = gameObject.AddComponent() as UserGUI;
}
void Update()
{
if (running)
{
count++;
if (Input.GetButtonDown("Fire1"))
{
Vector3 pos = Input.mousePosition;
Hit(pos);
}
switch (round)
{
case 1:
{
if (stop)
{
count = 0;
hold++;
if (hold == 300)
{
stop = false;
hold = 0;
}
}
if (count >= 180)
{
count = 0;
SendDisk(1);
trial += 1;
if (trial == 10)
{
stop = true;
hold = 0;
round += 1;
trial = 0;
}
}
break;
}
case 2:
{
if (stop)
{
count = 0;
hold++;
if (hold == 600)
{
stop = false;
hold = 0;
}
}
if (count >= 120)
{
count = 0;
if (trial % 2 == 0) SendDisk(1);
else SendDisk(2);
trial += 1;
if (trial == 10)
{
stop = true;
hold = 0;
round += 1;
trial = 0;
}
}
break;
}
case 3:
{
if (stop)
{
count = 0;
hold++;
if (hold == 600)
{
stop = false;
hold = 0;
}
}
if (count >= 60)
{
count = 0;
if (trial % 3 == 0) SendDisk(1);
else if (trial % 3 == 1) SendDisk(2);
else SendDisk(3);
trial += 1;
if (trial == 10)
{
stop = true;
hold = 0;
}
}
break;
}
default:
{
if (stop)
{
hold++;
if (hold == 300)
{
running = false;
stop = false;
hold = 0;
}
}
break;
}
}
disk_factory.FreeDisk();
}
}
public void LoadResources()
{
disk_factory.GetDisk(round);
disk_factory.FreeDisk();
}
private void SendDisk(int type)
{
GameObject disk = disk_factory.GetDisk(type);
float ran_y = 0;
float ran_x = Random.Range(-1f, 1f) < 0 ? -1 : 1;
float power = 0;
float angle = 0;
if (type == 1)
{
ran_y = Random.Range(2f, 5f);
power = Random.Range(5f, 8f);
angle = Random.Range(15f, 20f);
}
else if (type == 2)
{
ran_y = Random.Range(3f, 6f);
power = Random.Range(10f, 13f);
angle = Random.Range(10f, 15f);
}
else
{
ran_y = Random.Range(1f, 3f);
power = Random.Range(15f, 18f);
angle = Random.Range(5f, 10f);
}
disk.transform.position = new Vector3(ran_x * 16f, ran_y, 0);
action_manager.DiskFly(disk, angle, power);
}
public void Hit(Vector3 pos)
{
Ray ray = Camera.main.ScreenPointToRay(pos);
RaycastHit[] hits;
hits = Physics.RaycastAll(ray);
for (int i = 0; i < hits.Length; i++)
{
RaycastHit hit = hits[i];
if (hit.collider.gameObject.GetComponent() != null)
{
judge.Record(hit.collider.gameObject);
hit.collider.gameObject.transform.position = new Vector3(0, -10, 0);
}
}
}
public float GetScore()
{
return judge.GetScore();
}
public int GetRound()
{
return round;
}
public int GetTrial()
{
return trial;
}
public bool GetStop()
{
return stop;
}
public int GetHold()
{
return hold;
}
public void ReStart()
{
running = true;
judge.Reset();
disk_factory.Reset();
round = 1;
trial = 0;
stop = true;
}
public void GameOver()
{
running = false;
}
}
包括了飞碟类型type,分数score,颜色color三个属性,可以通过这些属性设置飞碟预制件的属性
public class Disk : MonoBehaviour
{
public int type = 1;
public int score = 1;
public Color color = Color.white;
}
Disk的工厂。主要进行飞碟的生产与回收。
在本函数之中,DiskFactory维护两个列表,一个是使用中的飞碟,一个是空闲飞碟。当场景控制器需要获取一个飞碟时,先在空闲列表中寻找可用的空闲飞碟,如果找不到就根据预制重新实例化一个飞碟。回收飞碟的逻辑为遍历使用列表,当有飞碟已经完成了所有动作,即位置在摄像机之下,则回收。
public class DiskFactory : MonoBehaviour
{
private List used = new List();
private List free = new List();
public GameObject GetDisk(int type)
{
GameObject disk_prefab = null;
//寻找空闲飞碟,如果无空闲飞碟则重新实例化飞碟
if (free.Count > 0)
{
for (int i = 0; i < free.Count; i++)
{
if (free[i].type == type)
{
disk_prefab = free[i].gameObject;
free.Remove(free[i]);
break;
}
}
}
if (disk_prefab == null)
{
if (type == 1)
{
disk_prefab = Instantiate(
Resources.Load("Prefabs/disk1"),
new Vector3(0, -10f, 0), Quaternion.identity);
}
else if (type == 2)
{
disk_prefab = Instantiate(
Resources.Load("Prefabs/disk2"),
new Vector3(0, -10f, 0), Quaternion.identity);
}
else
{
disk_prefab = Instantiate(
Resources.Load("Prefabs/disk3"),
new Vector3(0, -10f, 0), Quaternion.identity);
}
disk_prefab.GetComponent().material.color = disk_prefab.GetComponent().color;
}
used.Add(disk_prefab.GetComponent());
disk_prefab.SetActive(true);
return disk_prefab;
}
public void FreeDisk()
{
for (int i = 0; i < used.Count; i++)
{
if (used[i].gameObject.transform.position.y <= -10f)
{
free.Add(used[i]);
used.Remove(used[i]);
}
}
}
public void Reset()
{
FreeDisk();
}
}
UI代码,显示回合切换以及游戏开始结束、游戏进行时的UI。
public class UserGUI : MonoBehaviour
{
private IUserAction action;
//每个GUI的style
GUIStyle bold_style = new GUIStyle();
GUIStyle text_style = new GUIStyle();
GUIStyle over_style = new GUIStyle();
GUIStyle round_style = new GUIStyle();
private bool game_start = false;
private int wid = Screen.width;
private int heig = Screen.height;
void Start()
{
action = SSDirector.GetInstance().CurrentSceneController as IUserAction;
}
void OnGUI()
{
bold_style.normal.textColor = new Color(1, 0, 0);
bold_style.fontSize = 16;
text_style.normal.textColor = new Color(0, 0, 0, 1);
text_style.fontSize = 16;
over_style.normal.textColor = new Color(1, 0, 0);
over_style.fontSize = 25;
round_style.normal.textColor = new Color(1, 0, 0);
round_style.fontSize = 70;
if (game_start && !action.GetStop())
{
GUI.Label(new Rect(wid - 150, 5, 200, 50), "分数:" + action.GetScore().ToString(), text_style);
GUI.Label(new Rect(100, 5, 50, 50), "Round:" + action.GetRound().ToString(), text_style);
GUI.Label(new Rect(180, 5, 50, 50), "Trial:" + action.GetTrial().ToString(), text_style);
if (action.GetRound() == 3 && action.GetTrial() == 10)
{
GUI.Label(new Rect(wid / 2 - 20, heig / 2 - 100, 100, 100), "游戏结束", over_style);
GUI.Label(new Rect(wid / 2 - 10, heig / 2 - 50, 50, 50), "你的分数:" + action.GetScore().ToString(), text_style);
if (GUI.Button(new Rect(wid / 2 - 20, heig / 2, 100, 50), "重新开始"))
{
action.ReStart();
return;
}
action.GameOver();
}
}
else if (game_start && action.GetStop())
{
if (action.GetHold() > 300 && action.GetRound() <= 3 && action.GetTrial() == 0)
{
GUI.Label(new Rect(wid / 2 - 110, heig / 2 - 60, 100, 100), "Round" + (action.GetRound()).ToString(), round_style);
}
if (action.GetRound() == 1)
{
GUI.Label(new Rect(wid / 2 - 110, heig / 2 - 60, 100, 100), "Round" + (action.GetRound()).ToString(), round_style);
}
}
else
{
GUI.Label(new Rect(wid / 2 - 60, heig / 2 - 100, 100, 100), "简单打飞碟", over_style);
GUI.Label(new Rect(wid / 2 - 50, heig / 2 - 50, 100, 100), "鼠标点击飞碟", text_style);
if (GUI.Button(new Rect(wid / 2 - 50, heig / 2, 100, 50), "游戏开始"))
{
game_start = true;
action.ReStart();
}
}
}
}
演示gif图入下:
提醒:本次游戏项目在运行时是默认打开垂直同步的,开关在如下图所示位置:
如果关闭了垂直同步,游戏帧率会变高,但是也会体验到地狱绘卷难度的打飞碟,如下所示:
代码以及文档均已经上传至hw5 · XiaoChen04_3/3D_Computer_Game_Programming - gitee