3D游戏编程与设计 HW 5 与游戏世界交互
Hit UFO 完整游戏过程可见以下视频:
https://www.bilibili.com/video/BV1ni4y1E7gq/
Hit UFO 完整代码可见以下仓库:
https://gitee.com/beilineili/game3-d
1、编写一个简单的鼠标打飞碟(Hit UFO)游戏
① 游戏内容要求
- 游戏有 n 个 round,每个 round 都包括10 次 trial;
- 每个 trial 的飞碟的色彩、大小、发射位置、速度、角度、同时出现的个数都可能不同。它们由该 round 的 ruler 控制;
- 每个 trial 的飞碟有随机性,总体难度随 round 上升;
- 鼠标点中得分,得分规则按色彩、大小、速度不同计算,规则可自由设定。
② 游戏的要求
- 使用带缓存的工厂模式管理不同飞碟的生产与回收,该工厂必须是场景单实例的!具体实现见参考资源 Singleton 模板类
- 尽可能使用前面 MVC 结构实现人机交互与游戏模型分离
2、游戏设计
- 游戏界面
① 游戏玩法
- 通过用鼠标点击飞出来的UFO来得分,其中绿色飞碟为1分,黄色飞碟为2分,红色飞碟为3分
- 分为四个模式,分为简单,普通,困难和无限模式。其中开始时游戏默认是 Normal 模式
- 除了无限模式有无限轮次的飞碟外,其他模式都是有10轮,所有飞碟发射完毕时游戏结束
② 设计模式 – 工厂方法
- 简单工厂又称为工厂方法,即类一个方法能够得到一个对象实例,使用者不需要知道该实例如何构建、初始化等细节。
- 使用工厂方法的原因:
- 游戏对象的创建与销毁高成本,必须减少销毁次数。如:游戏中子弹
- 屏蔽创建与销毁的业务逻辑,使程序易于扩展
③ 设计代码
一、Controller
1) SSDirector.cs
- 导演类
public class SSDirector : System.Object
{
//单实例
private static SSDirector instance;
public ISceneController CurrentScenceController { get; set; }
//在任何时候任何地方获得实例
public static SSDirector GetInstance(){
if (instance == null){
instance = new SSDirector();
}
return instance;
}
}
2) SSAction.cs
- 动作基类
public class SSAction : ScriptableObject
{
public bool enable = true; //是否进行
public bool destroy = false; //是否删除
//需要进行运动的游戏对象
public GameObject gameObject { get; set; }
public Transform transform { get; set; }
//动作执行完后要通知的对象
public ISSActionCallback callback { get; set; }
protected SSAction()
{
}
// 申明虚方法,通过重写实现多态,由继承者来明确行为
public virtual void Start()
{
throw new System.NotImplementedException();
}
public virtual void Update()
{
throw new System.NotImplementedException();
}
}
3) SSActionManager.cs
- 动作管理者的基类
public class SSActionManager : MonoBehaviour
{
//动作集
private Dictionary<int, SSAction> actions = new Dictionary<int, SSAction>();
//等待执行的动作的list
private List<SSAction> waitingTodo = new List<SSAction>();
//等待删除的动作的key list
private List<int> waitingDelete = new List<int>();
protected void Update()
{
//将等待执行list中的动作保存进动作集
foreach (SSAction ac in waitingTodo)
actions[ac.GetInstanceID()] = ac;
waitingTodo.Clear();
//运行被保存的动作
foreach (KeyValuePair<int, SSAction> kv in actions)
{
SSAction ac = kv.Value;
if (ac.destroy)
{
waitingDelete.Add(ac.GetInstanceID());
}
else if (ac.enable)
{
ac.Update();
}
}
//销毁等待删除list中的动作
foreach (int key in waitingDelete)
{
SSAction ac = actions[key];
actions.Remove(key);
Destroy(ac);
}
waitingDelete.Clear();
}
//准备执行一个动作,将动作初始化,并加入到等待执行list
public void RunAction(GameObject gameObject, SSAction action, ISSActionCallback manager)
{
action.gameObject = gameObject;
action.transform = gameObject.transform;
action.callback = manager;
waitingTodo.Add(action);
action.Start();
}
protected void Start()
{
}
}
4) ISSActionCallback.cs
- 回调函数接口
public enum SSActionEventType : int { Started, Competed }
public interface ISSActionCallback
{
//回调函数
void SSActionEvent(SSAction source,
SSActionEventType events = SSActionEventType.Competed,
int intParam = 0,
string strParam = null,
Object objectParam = null);
}
5) ISceneController.cs
- 场景控制类接口
public interface ISceneController
{
void LoadResources();
}
6) IUserAction.cs
- 用户动作接口,有点击,重新开始和选择游戏模式三个函数的接口
public interface IUserAction
{
void Hit(Vector3 position); //点击
void Restart(); //重新开始
void SetMode(int mode); //选择游戏模式
}
7) FlyAction.cs
- 将飞行运动拆分成水平和竖直两个方向,其中水平方向速度恒定,竖直方向添加重力加速度
- 当飞碟到达底部时,即飞碟的高度在摄像机观察范围之下时,动作结束,将进行回调
public class FlyAction : SSAction
{
float gravity; //重力加速度
float speed; //初始速度
float time; //时间
Vector3 direction; //初始飞行方向
public static FlyAction GetSSAction(Vector3 direction, float speed)
{
FlyAction action = ScriptableObject.CreateInstance<FlyAction>();
action.gravity = 9.8f;
action.time = 0;
action.speed = speed;
action.direction = direction;
return action;
}
public override void Start()
{
}
public override void Update()
{
time += Time.deltaTime;
//竖直方向上有重力加速度
transform.Translate(Vector3.down * gravity * time * Time.deltaTime);
//初始飞行方向匀速运动
transform.Translate(direction * speed * Time.deltaTime);
//飞碟到达画面底部,动作结束,进行回调
if (this.transform.position.y < -6)
{
this.destroy = true;
this.enable = false;
this.callback.SSActionEvent(this);
}
}
}
8) ActionManager.cs
- 飞行动作管理类,用于生成飞碟飞行动作,和接受飞行动作的回调信息,回收飞碟
public class ActionManager : SSActionManager, ISSActionCallback
{
//飞行动作
public FlyAction flyAction;
//控制器
public FirstController controller;
protected new void Start()
{
controller = (FirstController)SSDirector.GetInstance().CurrentScenceController;
controller.actionManager = this;
}
public void Fly(GameObject disk, float speed, Vector3 direction)
{
flyAction = FlyAction.GetSSAction(direction, speed);
RunAction(disk, flyAction, this);
}
//回调函数
public void SSActionEvent(SSAction source,
SSActionEventType events = SSActionEventType.Competed,
int intParam = 0,
string strParam = null,
Object objectParam = null)
{
//飞碟结束飞行后工厂进行回收
controller.diskFactory.FreeDisk(source.gameObject);
}
}
9) FirstController.cs
- 场景控制器,负责游戏逻辑
- 发送飞碟 SendDisk – 从工厂获得一个飞碟并设置初始位置和速度,飞行动作
- 点击判断 Hit – 处理用户点击动作,将被点击到的飞碟回收,计算得分
在游戏中,玩家通过鼠标点击飞碟,从而得分。这当中涉及到一个点击判断的问题。我们调用 ScreenPointToRay 方法,构造由摄像头和屏幕点击点确定的射线,与射线碰撞的游戏对象即为玩家点击的对象
public class FirstController : MonoBehaviour, ISceneController, IUserAction
{
public ActionManager actionManager; //动作管理者
public DiskFactory diskFactory; //飞碟工厂
int[] roundDisks; //对应轮次的飞碟数量
int mode; //当前模式,简单-0,正常-1,困难-2,或者无限模式-3
int points; //当前分数
int round; //当前轮次
int sendCount; //当前已发送的飞碟数量
float sendTime; //发送时间
void Start()
{
LoadResources();
}
public void LoadResources()
{
SSDirector.GetInstance().CurrentScenceController = this;
gameObject.AddComponent<DiskFactory>();
gameObject.AddComponent<ActionManager>();
gameObject.AddComponent<UserGUI>();
diskFactory = Singleton<DiskFactory>.Instance;
points = 0;
round = 1;
sendCount = 0;
sendTime = 0;
mode = 1;
roundDisks = new int[] { 1, 1, 2, 3, 5, 8, 13, 21, 34, 55 };
}
//用于发射一个飞碟
public void SendDisk(int mode)
{
//生成一个飞碟
GameObject disk = diskFactory.GetDisk(round-1);
//设置飞碟的初始位置
disk.transform.position = new Vector3(-disk.GetComponent<DiskData>().direction.x * 7, UnityEngine.Random.Range(0f, 8f), 0);
disk.SetActive(true);
//设置飞碟的飞行动作
if (mode == 0) {
actionManager.Fly(disk, disk.GetComponent<DiskData>().speed*0.5f, disk.GetComponent<DiskData>().direction);
}
else if (mode == 2) {
actionManager.Fly(disk, disk.GetComponent<DiskData>().speed*1.5f, disk.GetComponent<DiskData>().direction);
}
else {
actionManager.Fly(disk, disk.GetComponent<DiskData>().speed, disk.GetComponent<DiskData>().direction);
}
}
//处理用户的点击动作
public void Hit(Vector3 position)
{
Camera ca = Camera.main;
Ray ray = ca.ScreenPointToRay(position);
RaycastHit[] hits;
hits = Physics.RaycastAll(ray);
for (int i = 0; i < hits.Length; i++)
{
RaycastHit hit = hits[i];
//如果用户点击到飞碟
if (hit.collider.gameObject.GetComponent<DiskData>() != null)
{
//将飞碟移至底部,触发飞行动作的回调
hit.collider.gameObject.transform.position = new Vector3(0, -7, 0);
//分数增加
points += hit.collider.gameObject.GetComponent<DiskData>().points;
//更新GUI
gameObject.GetComponent<UserGUI>().points = points;
}
}
}
//重新开始
public void Restart()
{
gameObject.GetComponent<UserGUI>().result = "";
round = 1;
sendCount = 0;
points = 0;
gameObject.GetComponent<UserGUI>().points = points;
gameObject.GetComponent<UserGUI>().round = round;
gameObject.GetComponent<UserGUI>().mode = mode;
}
//设置模式
public void SetMode(int mode)
{
this.mode = mode;
}
//发射飞碟
void Update()
{
sendTime += Time.deltaTime;
//每1s发送一次飞碟
if (sendTime > 1)
{
sendTime = 0;
//每次发送至多5个飞碟,且不能多于当前 round 的最大飞碟数量
for (int i = 0; i < 5 && sendCount < roundDisks[round-1]; i++)
{
sendCount++;
SendDisk(mode);
}
//过了10 round,判断是否是无限模式,不是则输出游戏结束
if (sendCount == roundDisks[round-1] && round == roundDisks.Length)
{
if (mode == 3)
{
round = 1;
sendCount = 0;
gameObject.GetComponent<UserGUI>().result = "";
}
else
{
gameObject.GetComponent<UserGUI>().result = "Game Over!";
}
}
//一轮发送的飞碟数量够了,更新轮次
if (sendCount == roundDisks[round-1] && round < roundDisks.Length)
{
sendCount = 0;
round++;
gameObject.GetComponent<UserGUI>().round = round;
}
}
}
}
二、Model
1) Singleton.cs
- 单实例类,当所需的实例被请求时,如果还不存在就先创建,如果存在就直接返回
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));
if (instance == null)
{
Debug.LogError("An instance of " + typeof(T) + " is needed in the scene, but there is none");
}
}
return instance;
}
}
}
2) DiskData.cs
- 飞碟数据类,包含飞碟的初始速度、击中时的得分、以及初始飞行方向
public class DiskData : MonoBehaviour
{
public float speed; //初始速度
public int points; //击中的得分
public Vector3 direction; //初始移动方向
}
3) DiskFactory.cs
- DiskFactory 类是一个单实例类,用单实例创建
- DiskFactory 类有工厂方法 GetDisk 产生飞碟,有回收方法 FreeDisk;使用两个列表来分别维护正在使用和未被使用的飞碟对象
- DiskFactory 使用模板模式根据预制和规则制作飞碟
在游戏中,对象的新建和销毁的开销是巨大的,是不可忽视的。对于频繁出现的游戏对象,我们应该使用对象池技术缓存,从而降低对象的新建和销毁开销。在本游戏中,飞碟是频繁出现的游戏对象,我们使用带缓存的工厂模式管理不同飞碟的生产和回收。对于该飞碟工厂,我们使用单例模式
对象池的实现(伪代码)
getDisk(ruler)
BEGIN
IF (free list has disk) THEN
a_disk = remove one from list
ELSE
a_disk = clone from Prefabs
ENDIF
Set DiskData of a_disk with the ruler
Add a_disk to used list
Return a_disk
END
FreeDisk(disk)
BEGIN
Find disk in used list
IF (not found) THEN THROW exception
Move disk from used to free list
END
DiskFactory
//带缓存的工厂模式管理不同飞碟的生产与回收
public class DiskFactory : MonoBehaviour
{
public GameObject diskPrefab; //飞碟预制
//生产含有基本属性的飞碟
private List<DiskData> use; //正被使用的飞碟
private List<DiskData> free; //空闲的飞碟
public void Start()
{
use = new List<DiskData>();
free = new List<DiskData>();
diskPrefab = GameObject.Instantiate<GameObject>(Resources.Load<GameObject>("Prefabs/Disk"), Vector3.zero, Quaternion.identity);
diskPrefab.SetActive(false);
}
//生产飞碟
public GameObject GetDisk(int round)
{
GameObject disk;
//如果有空闲的飞碟就直接使用,否则生成新的
if (free.Count > 0)
{
disk = free[0].gameObject;
free.Remove(free[0]);
}
else
{
disk = GameObject.Instantiate<GameObject>(diskPrefab, Vector3.zero, Quaternion.identity);
disk.AddComponent<DiskData>();
}
//飞碟的等级 = 0~2之间的随机数 * 轮次数
//轮次 round 越高,出的飞碟分数越高,速度也越快
float level = UnityEngine.Random.Range(0, 2f) * (round + 1);
if (level < 4)
{
disk.GetComponent<DiskData>().points = 1;
disk.GetComponent<DiskData>().speed = 4.0f;
disk.GetComponent<DiskData>().direction = new Vector3(UnityEngine.Random.Range(-1f, 1f) > 0 ? 2 : -2, 1, 0);
disk.GetComponent<Renderer>().material.color = Color.green; //绿色飞碟,速度最慢
}
else if (level >= 4 && level < 7)
{
disk.GetComponent<DiskData>().points = 2;
disk.GetComponent<DiskData>().speed = 6.0f;
disk.GetComponent<DiskData>().direction = new Vector3(UnityEngine.Random.Range(-1f, 1f) > 0 ? 2 : -2, 1, 0);
disk.GetComponent<Renderer>().material.color = Color.yellow; //黄色飞碟,速度中等
}
else
{
disk.GetComponent<DiskData>().points = 3;
disk.GetComponent<DiskData>().speed = 8.0f;
disk.GetComponent<DiskData>().direction = new Vector3(UnityEngine.Random.Range(-1f, 1f) > 0 ? 2 : -2, 1, 0);
disk.GetComponent<Renderer>().material.color = Color.red; //红色飞碟,速度最快
}
use.Add(disk.GetComponent<DiskData>());
return disk;
}
//回收飞碟
public void FreeDisk(GameObject disk)
{
//找到使用中的飞碟,将其回收
foreach (DiskData diskData in use)
{
if (diskData.gameObject.GetInstanceID() == disk.GetInstanceID())
{
disk.SetActive(false);
free.Add(diskData);
use.Remove(diskData);
break;
}
}
}
}
三、View
- UserGUI 界面类,用于构建 UI 和捕捉用户动作,将分数,Round,游戏模式显示出来
public class UserGUI : MonoBehaviour
{
private IUserAction userAction;
public string result;
public int points;
public int round;
public int mode;
void Start()
{
points = 0;
round = 1;
mode = 1;
result = "";
userAction = SSDirector.GetInstance().CurrentScenceController as IUserAction;
}
//打印和用户交互提示界面
void OnGUI()
{
GUIStyle titleStyle = new GUIStyle();
titleStyle.normal.textColor = Color.black;
titleStyle.fontSize = 50;
GUIStyle style = new GUIStyle();
style.normal.textColor = Color.white;
style.fontSize = 30;
GUIStyle resultStyle = new GUIStyle();
resultStyle.normal.textColor = Color.red;
resultStyle.fontSize = 50;
GUI.Label(new Rect(600, 30, 50, 200), "Hit UFO", titleStyle);
GUI.Label(new Rect(20, 10, 100, 50), "Points: " + points, style);
GUI.Label(new Rect(220, 10, 100, 50), "Round: " + round, style);
GUI.Label(new Rect(800, 100, 50, 200), result, resultStyle);
if (GUI.Button(new Rect(1300, 100, 100, 50), "Restart"))
{
userAction.Restart();
}
//简单模式
if (GUI.Button(new Rect(1300, 200, 100, 50), "Easy Mode"))
{
userAction.SetMode(0);
}
//通常模式
if (GUI.Button(new Rect(1300, 300, 100, 50), "Normal Mode"))
{
userAction.SetMode(1);
}
//困难模式
if (GUI.Button(new Rect(1300, 400, 100, 50), "Hard Mode"))
{
userAction.SetMode(2);
}
//无限模式
if (GUI.Button(new Rect(1300, 500, 100, 50), "Infinite Mode"))
{
userAction.SetMode(3);
}
string[] s = new string[]{"Easy","Normal","Hard","Infinite"};
GUI.Label(new Rect(600, 100, 100, 50), s[mode] + " Mode", style);
//捕捉鼠标点击
if (Input.GetButtonDown("Fire1"))
{
userAction.Hit(Input.mousePosition);
}
}
}
3. 游戏画面
- 简单模式
- 困难模式下游戏结束