3D Computer Game Programming-Note 5
1、编写一个简单的鼠标打飞碟(Hit UFO)游戏
- 游戏内容要求:
- 游戏有 n 个 round,每个 round 都包括 10 次 trial;
- 每个 trial 的飞碟的色彩、大小、发射位置、速度、角度、同时出现的个数都可能不同。它们由该 round 的 ruler 控制;
- 每个 trial 的飞碟有随机性,总体难度随 round 上升;
- 鼠标点中得分,得分规则按色彩、大小、速度不同计算,规则可自由设定。
- 游戏的要求:
- 使用带缓存的工厂模式管理不同飞碟的生产与回收,该工厂必须是场景单实例的!具体实现见参考资源 Singleton 模板类
- 近可能使用前面 MVC 结构实现人机交互与游戏模型分离
【游戏设计】
游戏规则:
· 在飞碟飞过时点击鼠标左键,尽可能击中更多的飞碟
· 游戏共分四个轮次,随着游戏的进行难度逐渐增加,具体表现为飞碟飞行速度的提升和飞行角度的变化
· 不同颜色的飞碟得分不同,击中难度也不同
【结构设计】
参考游戏牧师与魔鬼(动作分离版)的 MVC 架构,保留SSDirector,SSAction和SSActionManager 等,重复部分略过不表。
使用带缓存的工厂模式管理飞碟,要求:
- DiskFactory 类是一个单实例类,用前面场景单实例创建
- DiskFactory 类有工厂方法 GetDisk 产生飞碟,有回收方法 Free(Disk)
- DiskFactory 使用模板模式根据预制和规则制作飞碟
- 对象模板包括飞碟对象与飞碟数据
借助课程主页的设计图,设计程序的 UML图:
【编程实现】
- Singleton.cs: 模板类,可以为每个 MonoBehaviour 子类创建一个对象的实例
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;
}
}
}
- DiskData.cs: 记录飞碟数据,包括飞碟的类型、对应得分等,挂载到预制上时可以实现自定义组件
public class DiskData : MonoBehaviour {
public int type = 1;
public int score = 1;
public Color color = Color.white;
}
- DiskFactory.cs: 提供工厂方法 GetDisk 产生飞碟以及 FreeDisk 回收飞碟
public class DiskFactory : MonoBehaviour {
private List<DiskData> used;
private List<DiskData> free;
void Start() {
used = new List<DiskData>();
free = new List<DiskData>();
}
public GameObject GetDisk(int type) {
GameObject disk = null;
if (free.Count > 0) {
for (int i = 0; i < free.Count; i++) {
if (free[i].type == type) {
disk = free[i].gameObject;
free.Remove(free[i]);
break;
}
}
}
float random_y = Random.Range(-3f, 1f);
float random_x = Random.Range(-1f, 1f) < 0 ? -1 : 1;
if (disk == null)
disk = Instantiate(Resources.Load<GameObject>("Prefabs/disk"+type), new Vector3(0, -20f, 0), Quaternion.identity);
disk.transform.position = new Vector3(random_x * 20f, random_y, 0);
disk.GetComponent<Renderer>().material.color = disk.GetComponent<DiskData>().color;
used.Add(disk.GetComponent<DiskData>());
disk.SetActive(true);
return disk;
}
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);
used.Remove(used[i]);
free.Add(used[i]);
break;
}
}
}
public void Reset() {
for (int i = 0; i < used.Count; i++) {
if (used[i].gameObject.transform.position.y <= -20f) {
free.Add(used[i]);
used.Remove(used[i]);
}
}
}
}
- FlyActionManager.cs: 管理飞碟的飞行,场景控制器可以通过该管理器使飞碟按照一定的角度和速度飞行
public class FlyActionManager : SSActionManager {
public DiskFlyAction flyAction;
public FirstController sceneController;
protected void Start() {
sceneController = (FirstController)SSDirector.GetInstance().CurrentSceneController;
sceneController.actionManager = this;
}
public void DiskFly(GameObject disk, float angle, float speed) {
int direction = (disk.transform.position.x > 0) ? -1 : 1;
flyAction = DiskFlyAction.GetSSAction(direction, angle, speed);
this.RunAction(disk, flyAction, this);
}
}
- DiskFlyAction.cs: 使用旋转实现飞行。当飞碟到达指定的水平面时动作结束
public class DiskFlyAction : SSAction {
public float gravity = -0.2f;
private Vector3 speed;
private Vector3 gravityVector = Vector3.zero;
private Vector3 angle = Vector3.zero;
private float time;
private DiskFlyAction() { }
public static DiskFlyAction GetSSAction(int direction, float angle, float speed) {
DiskFlyAction action = ScriptableObject.CreateInstance<DiskFlyAction>();
if (direction == -1) {
action.speed = Quaternion.Euler(new Vector3(0, 0, -angle)) * Vector3.left * speed;
}
else {
action.speed = Quaternion.Euler(new Vector3(0, 0, angle)) * Vector3.right * speed;
}
return action;
}
public override void Start() { }
public override void Update() {
time += Time.deltaTime;
gravityVector.y = gravity * time;
transform.position += (speed + gravityVector) * Time.fixedDeltaTime;
angle.z = Mathf.Atan((speed.y + gravityVector.y) / speed.x) * Mathf.Rad2Deg;
transform.eulerAngles = angle;
if (this.transform.position.y < -20f) {
this.destroy = true;
this.callback.SSActionEvent(this);
}
}
}
- ScoreRecorder.cs: 记分员,根据飞碟的数据计分,提供对计分表的读写
public class ScoreRecorder : MonoBehaviour {
private int score;
void Start () {
score = 0;
}
public void Record(GameObject disk) {
score += disk.GetComponent<DiskData>().score;
}
public int GetScore() {
return score;
}
public void Reset() {
score = 0;
}
}
- IUserAction.cs: 提供用户交互事件的接口,包括击中飞碟、重新开始游戏等
public interface IUserAction
{
void Hit(Vector3 position);
void ReStart();
int GetScore();
int GetRound();
bool isGameOver();
void GameOver();
}
- FirstController.cs: 场景控制器,管理飞碟和计分表等对象及相关事件
public class FirstController : MonoBehaviour, ISceneController, IUserAction {
public FlyActionManager actionManager;
public DiskFactory diskFactory;
public ScoreRecorder scoreRecorder;
private int round = 1;
private int trial = 0;
private bool running = false;
private int count = 0;
void Start () {
SSDirector.GetInstance().CurrentSceneController = this;
diskFactory = Singleton<DiskFactory>.Instance;
scoreRecorder = Singleton<ScoreRecorder>.Instance;
gameObject.AddComponent<FlyActionManager>();
gameObject.AddComponent<UserGUI>();
}
void Update () {
if(running) {
count++;
//用户按下鼠标左键时进行hit事件处理
if (Input.GetButtonDown("Fire1")) {
Hit(Input.mousePosition);
}
//根据游戏轮次设计飞碟的类型和发射速率
int speed = 300 - round * 50;
if (count >= speed)
{
count = 0;
float rand;
switch (round)
{
case 1:
SendDisk(1);
break;
case 2:
rand = Random.Range(0, 1f);
if (rand < 0.6f)
SendDisk(1);
else
SendDisk(2);
break;
case 3:
rand = Random.Range(0, 1f);
if (rand < 0.4f)
SendDisk(1);
else if (rand < 0.8f)
SendDisk(2);
else
SendDisk(3);
break;
case 4:
rand = Random.Range(0, 1f);
if (rand < 0.2f)
SendDisk(1);
else if (rand < 0.5f)
SendDisk(2);
else
SendDisk(3);
break;
default:
break;
}
trial += 1;
if (trial == 10)
{
if (round == 4)
{
running = false;
}
else
{
round += 1;
trial = 0;
}
}
}
diskFactory.Reset();
}
}
public void LoadResources() {
diskFactory.GetDisk(round);
diskFactory.Reset();
}
private void SendDisk(int type) {
GameObject disk = diskFactory.GetDisk(type);
float speed = 0;
float angle = 0;
if (type == 1)
{
speed = Random.Range(5f, 10f);
angle = Random.Range(10f, 14f);
}
else if (type == 2)
{
speed = Random.Range(8f, 13f);
angle = Random.Range(12f, 16f);
}
else
{
speed = Random.Range(13f, 18f);
angle = Random.Range(16f, 20f);
}
actionManager.DiskFly(disk, angle, speed);
}
//处理hit事件
public void Hit(Vector3 position) {
Ray ray = Camera.main.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) {
scoreRecorder.Record(hit.collider.gameObject);
hit.collider.gameObject.transform.position = new Vector3(0, -20f, 0);
}
}
}
public int GetScore() {
return scoreRecorder.GetScore();
}
public int GetRound() {
return round;
}
public bool isGameOver()
{
return round == 4 && trial == 10;
}
public void ReStart() {
running = true;
scoreRecorder.Reset();
diskFactory.Reset();
round = 1;
trial = 1;
}
public void GameOver() {
running = false;
}
}
【游戏效果】
【动态展示】
2、编写一个简单的自定义 Component (选做)
- 用自定义组件定义几种飞碟,做成预制
- 实现自定义组件,编辑并赋予飞碟一些属性
使用 Sphere 和 Capsule 组合制作预制如下:
![](https://cdn.nlark.com/yuque/0/2020/png/2589459/1603882158743-1a47a714-13b2-486f-8fa1-630ac70e2bf9.png#align=left&display=inline&height=127&margin=%5Bobject%20Object%5D&originHeight=127&originWidth=287&size=0&status=done&style=none&width=287)
在预制上挂载 DiskData.cs,即可编辑飞碟的属性:类型、得分和颜色:
![](https://cdn.nlark.com/yuque/0/2020/png/2589459/1603882158561-1b2d1a6a-4318-4a1a-b922-67b7f98a2421.png#align=left&display=inline&height=463&margin=%5Bobject%20Object%5D&originHeight=463&originWidth=596&size=0&status=done&style=none&width=596)