编写一个简单的鼠标打飞碟(Hit UFO)游戏
游戏内容要求:
- 游戏有 n 个 round,每个 round 都包括10 次 trial;
- 每个 trial 的飞碟的色彩、大小、发射位置、速度、角度、同时出现的个数都可能不同。它们由该 round 的 ruler 控制;
- 每个 trial 的飞碟有随机性,总体难度随 round 上升;
- 鼠标点中得分,得分规则按色彩、大小、速度不同计算,规则可自由设定。
游戏的要求:
- 使用带缓存的工厂模式管理不同飞碟的生产与回收,该工厂必须是场景单实例的!具体实现见参考资源 Singleton 模板类
- 尽可能使用前面 MVC 结构实现人机交互与游戏模型分离
游戏规则
- 游戏总共分为3轮,每轮玩家共有5次生命,每漏掉一个飞盘就扣除一次生命,反之鼠标每点中一次飞盘,即可得到相应的分数;
- 逐轮增加难度,随着轮次的上升,飞碟的飞行速度会更快;
- 每个trail的飞碟的色彩,大小;发射位置,速度,角度,每次发射飞碟数量不一;
??由于每次出现一个飞碟就要创建一个对象实例,飞碟的销毁有需要销毁实例,这很大程度上增加了游戏运行成本,所以使用工厂模式。飞碟工厂用于单独管理预制之飞碟的创建和回收,飞碟工厂先创建一堆不同颜色的飞碟,然后需要飞碟时就从工厂中随机取出一个可用的飞碟,当飞碟不被需要时则将其放入工厂中,省去了飞碟实例创建和销毁的代价。
游戏项目的UML图如下
游戏实现
DiskFactory
飞碟工厂类中包含了飞碟的产生与回收,还包含了根据回合调整飞碟的飞出速度的设定。其中,使用了list来实现飞碟的产生和销毁,先创建飞碟实例放在free队列中,然后当需要飞碟的时候就从free队列中取,把使用过的飞碟放到used队列中。
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)
{
int choice = 0;
int scope1 = 1, scope2 = 4, scope3 = 7;
float start_y = -10f;
string tag;
disk_prefab = null;
if (round == 1)
{
choice = Random.Range(0, scope1);
}
else if(round == 2)
{
choice = Random.Range(0, scope2);
}
else
{
choice = Random.Range(0, scope3);
}
if(choice <= scope1)
{
tag = "disk1";
}
else if(choice <= scope2 && choice > scope1)
{
tag = "disk2";
}
else
{
tag = "disk3";
}
for(int i=0;i<free.Count;i++)
{
if(free[i].tag == tag)
{
disk_prefab = free[i].gameObject;
free.Remove(free[i]);
break;
}
}
if(disk_prefab == null)
{
if (tag == "disk1")
{
disk_prefab = Instantiate(Resources.Load<GameObject>("Prefabs/disk1"), new Vector3(0, start_y, 0), Quaternion.identity);
}
else if (tag == "disk2")
{
disk_prefab = Instantiate(Resources.Load<GameObject>("Prefabs/disk2"), new Vector3(0, start_y, 0), Quaternion.identity);
}
else
{
disk_prefab = Instantiate(Resources.Load<GameObject>("Prefabs/disk3"), new Vector3(0, start_y, 0), Quaternion.identity);
}
float ran_x = Random.Range(-1f, 1f) < 0 ? -1 : 1;
disk_prefab.GetComponent<Renderer>().material.color = disk_prefab.GetComponent<DiskData>().color;
disk_prefab.GetComponent<DiskData>().direction = new Vector3(ran_x, start_y, 0);
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;
}
}
}
}
DiskData
Diskdata,飞碟数据类用来给飞碟提供属性,点击到此飞碟能得到的分数,飞碟的颜色,飞碟飞出的速度与方向。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class DiskData : MonoBehaviour
{
public int score = 1;
public Color color = Color.red;
public float speed = 20;
public Vector3 direction;
}
RoundController
RoundController,场景控制器,场景控制器负责统筹管理游戏项目的每个组件,游戏一共分为三种状态:游戏开始,游戏进行中,游戏结束。根据游戏的情况,调用与飞碟共产的接口以使飞碟游戏的难度跟游戏轮次相匹配。场景控制器海涌到了定时器,相隔一定时间之后就发送飞碟,每当玩家漏点飞碟时更新相应的生命值。
public class RoundController : MonoBehaviour, ISceneController, IUserAction
{
public DiskFactory diskFactory;
public CCActionManager actionManager;
public ScoreRecorder scoreRecorder;
public UserGUI userGui;
private Queue<GameObject> diskQueue = new Queue<GameObject>();
private List<GameObject> diskMissed = new List<GameObject>();
private int totalRound = 3;
private int trialNumPerRound = 10;
private int currentRound = -1;
private int currentTrial = -1;
private float throwSpeed = 2f;
private int gameState = 0;
private float throwInterval = 0;
private int userBlood = 10;
void Awake()
{
SSDirector director = SSDirector.GetInstance();
director.CurrentSceneController = this;
diskFactory = Singleton<DiskFactory>.Instance;
userGui = gameObject.AddComponent<UserGUI>() as UserGUI;
actionManager = gameObject.AddComponent<CCActionManager>() as CCActionManager;
scoreRecorder = new ScoreRecorder();
}
public void LoadResource()
{
diskQueue.Enqueue(diskFactory.GetDisk(currentRound));
}
public void ThrowDisk(int count)
{
while(diskQueue.Count <= count)
{
LoadResource();
}
for(int i = 0; i < count; i++)
{
float position_x = 16;
GameObject disk = diskQueue.Dequeue();
diskMissed.Add(disk);
disk.SetActive(true);
float ran_y = Random.Range(-3f, 3f);
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 = Random.Range(10f, 15f);
float angle = Random.Range(15f, 28f);
actionManager.diskFly(disk, angle, power);
}
}
void levelUp()
{
currentRound += 1;
throwSpeed -= 0.5f;
currentTrial = 1;
}
void Update()
{
if(gameState == 1)
{
if(userBlood <= 0 || (currentRound == totalRound && currentTrial == trialNumPerRound))
{
GameOver();
return;
}
else
{
if (currentTrial > trialNumPerRound)
{
levelUp();
}
if (throwInterval > throwSpeed)
{
int throwCount = generateCount(currentRound);
ThrowDisk(throwCount);
throwInterval = 0;
currentTrial += 1;
}
else
{
throwInterval += Time.deltaTime;
}
}
}
for (int i = 0; i < diskMissed.Count; i++)
{
GameObject temp = diskMissed[i];
if (temp.transform.position.y < -8 && temp.gameObject.activeSelf == true)
{
diskFactory.FreeDisk(diskMissed[i]);
diskMissed.Remove(diskMissed[i]);
userBlood -= 1;
}
}
}
public int generateCount(int currentRound)
{
if(currentRound == 1)
{
return 1;
}
else if(currentRound == 2)
{
return Random.Range(1, 2);
}
else
{
return Random.Range(1, 3);
}
}
public void StartGame()
{
gameState = 1;
currentRound = 1;
currentTrial = 1;
userBlood = 10;
throwSpeed = 2f;
throwInterval = 0;
}
public void GameOver()
{
if(userBlood <= 0)
{
gameState = -1;
}
else
{
gameState = 2;
}
}
public void Restart()
{
scoreRecorder.Reset();
StartGame();
}
public void Hit(Vector3 pos)
{
Ray ray = Camera.main.ScreenPointToRay(pos);
RaycastHit[] hits;
hits = Physics.RaycastAll(ray);
bool notHit = false;
foreach (RaycastHit hit in hits)
if (hit.collider.gameObject.GetComponent<DiskData>() != null)
{
for (int j = 0; j < diskMissed.Count; j++)
{
if (hit.collider.gameObject.GetInstanceID() == diskMissed[j].gameObject.GetInstanceID())
{
notHit = true;
}
}
if (!notHit)
{
return;
}
diskMissed.Remove(hit.collider.gameObject);
scoreRecorder.Record(hit.collider.gameObject);
diskFactory.FreeDisk(hit.collider.gameObject);
break;
}
}
public int GetScore()
{
return scoreRecorder.GetScore();
}
public int GetCurrentRound()
{
return currentRound;
}
public int GetBlood()
{
return userBlood;
}
public int GetGameState()
{
return gameState;
}
}
游戏界面
游戏界面用来更新游戏和玩家的数据,包括得分数,生命值,当前回合等等。
public class UserGUI : MonoBehaviour
{
private IUserAction action;
private string score, round;
int blood, gameState, HighestScore;
void Start()
{
action = SSDirector.GetInstance().CurrentSceneController as IUserAction;
}
void Update()
{
gameState = action.GetGameState();
}
void OnGUI()
{
GUIStyle text_style;
GUIStyle button_style;
text_style = new GUIStyle()
{
fontSize = 20
};
button_style = new GUIStyle("button")
{
fontSize = 15
};
if (gameState == 0)
{
if (GUI.Button(new Rect(Screen.width / 2 - 50, 80, 100, 60), "Start Game", button_style))
{
action.StartGame();
}
}
else if(gameState == 1)
{
if (Input.GetButtonDown("Fire1"))
{
Vector3 mousePos = Input.mousePosition;
action.Hit(mousePos);
}
score = "Score: " + action.GetScore().ToString();
GUI.Label(new Rect(200, 5, 100, 100), score, text_style);
round = "Round: " + action.GetCurrentRound().ToString();
GUI.Label(new Rect(400, 5, 100, 100), round, text_style);
blood = action.GetBlood();
string bloodStr = "Blood: " + blood.ToString();
GUI.Label(new Rect(600, 5, 50, 50), bloodStr, text_style);
}
else
{
if (gameState == 2)
{
if (action.GetScore() > HighestScore) {
HighestScore = action.GetScore();
}
GUI.Label(new Rect(Screen.width / 2 - 50, Screen.height / 2 - 250, 100, 60), "Game Over", text_style);
string record = "Highest Score: " + HighestScore.ToString();
GUI.Label(new Rect(Screen.width / 2 - 70, Screen.height / 2 - 150, 150, 60), record, text_style);
}
else
{
GUI.Label(new Rect(Screen.width / 2 - 50, Screen.height / 2 - 150, 100, 70), "You Lost!", text_style);
}
if (GUI.Button(new Rect(Screen.width / 2 - 50, Screen.height / 2 - 30, 100, 60), "Restart", button_style))
{
action.Restart();
}
}
}
}
FlyAction
飞碟的飞行动作类,主要负责实现飞碟的飞行效果,包括给飞碟一个方向和力,让飞碟做带有类似重力加速的运动,当飞碟飞出游戏界面时将停止运动。
public class FlyAction : SSAction
{
public float gravity = -5;
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() { }
}
效果展示
开始界面
游戏进行中的界面 游戏结束界面
由于老师临时将第五,六章节的作业合并,所以接下来对已经做好的Hit UFO进行完善
本次游戏的完善主要是引入Adapter模式,目的是想在原来设计的基础上让游戏增加一个物理运动模式,但是又不想删除之前所做好的运动模式,即不想放弃CCActionManager,所以我们新建PhysisActionManager类。
新的项目类图:
相比于上面的UML类图,这个UML类图多了一个PhysisActionManager类,并且我们需要设计一个统一的抽象接口,这是为了能够使用我们已经创建的两种运动方式,由此我们可以对运动模式进行选择。
Adapter(适配器)模式
目的:将一个类的接口转换成客户希望的另外一个接口。适配器模式使得原本由于接口不兼容而不能一起工作的那些类可以一起工作。
主要解决在软件系统中,常常要将一些"现存的对象"放到新的环境中,而新环境要求的接口是现对象不能满足的。
项目实现
PhysisAction
这个类用于实现动力学运动模式,使得飞碟做自由落体运动
public class PhysisFlyAction : SSAction
{
private Vector3 startVector;
public float power;
public static PhysisFlyAction GetSSAction(Vector3 direction, float angle, float power)
{
PhysisFlyAction action = CreateInstance<PhysisFlyAction>();
if (direction.x == -1)
{
action.startVector = Quaternion.Euler(new Vector3(0, 1, -angle)) * Vector3.left * power;
}
else
{
action.startVector = Quaternion.Euler(new Vector3(0, 1, angle)) * Vector3.right * power;
}
action.power = power;
return action;
}
public override void Start()
{
gameObject.GetComponent<Rigidbody>().velocity = power / 15 * startVector;
gameObject.GetComponent<Rigidbody>().useGravity = true;
}
public override void Update()
{
if (this.transform.position.y < -10)
{
this.destory = true;
this.callback.SSActionEvent(this);
}
}
}
PhysisActionManager
对飞碟的物理运动进行管理
public class PhysisActionManager : SSActionManager, ISSActionCallback
{
public PhysisFlyAction fly;
protected new void Start(){ }
//飞碟飞行
public void playDisk(GameObject disk, float angle, float power)
{
fly = PhysisFlyAction.GetSSAction(disk.GetComponent<DiskData>().direction, angle, power);
this.RunAction(disk, fly, this);
}
#region ISSActionCallback implementation
public void SSActionEvent(SSAction source,
SSActionEventType events = SSActionEventType.Compeleted,
int intParam = 0,
string strParam = null,
Object objectParam = null)
{
//回调函数,动作执行完后调用
}
#endregion
}
ActionManagerAdapter
实现完飞碟的物理运动之后,我们需要实现一个运动管理适配器来对不同的运动模式进行管理和调度。
public class ActionManagerAdapter : MonoBehaviour, IActionManager
{
public CCActionManager CCAction;
public PhysisActionManager PhysisAction;
public void playDisk(GameObject disk, float angle, float power, bool isPhysis)
{
if (!isPhysis)
{
CCAction.playDisk(disk, angle, power);
}
else
{
PhysisAction.playDisk(disk, angle, power);
}
}
void Start()
{
CCAction = gameObject.AddComponent<CCActionManager>() as CCActionManager;
PhysisAction = gameObject.AddComponent<PhysisActionManager>() as PhysisActionManager;
}
}
当然,我们还需要修改场景控制器
使得在游戏过程中可以随时切换运动管理器
public bool isPhysis = false;
//将场景的运动管理器由原先的 CCActionManager 改为 IActionManager
actionManager = gameObject.AddComponent<ActionManagerAdapter>() as IActionManager;
//为原先的函数调用增加一个参数
actionManager.playDisk(disk, angle, power, isPhysis);
|