目录
- 一、游戏内容
- 二、UML图设计
- 三、游戏的实现
- 1. DiskFactory
- 2. FlyAction
- 3. SSActionManager
- 4. PhyUFOFlyAction
- 5. PhyFlyActionManager
- 6. ISceneController
- 7. UserGUI
- 四、 运行界面与代码传送门
一、游戏内容
改进飞碟(Hit UFO)游戏
游戏内容要求:
- 按 adapter模式 设计图修改飞碟游戏
- 使它同时支持物理运动与运动学(变换)运动
二、UML图设计
三、游戏的实现
1. DiskFactory
飞碟工厂实现对不同飞碟的生产、管理以及回收。需要注意的是,这里使用的是带缓存的单实例工厂模式。
- 单实例:运用模板,可以为每个 MonoBehaviour子类创建一个对象的实例:
Singleten<T>
- 带缓存的工厂:由于对飞碟的多次创建与销毁开销很大,所以我们使用带缓存的工厂来避免频繁的创建与销毁操作。当一个飞碟需要被销毁时,工厂并不直接销毁他,而是将其标记为“空闲”,表示他已经不被场景使用了。在新的飞碟被需要时,工厂不会直接去实例化一个新的飞碟,而是从被标记“空闲”的飞碟中选择可用的实例,只有在当前没有可用的实例时,工厂才会去真正实例化一个新的飞碟。这样一来就能减少游戏不断创建与销毁游戏对象的极大开销。```
public class DiskFactory : MonoBehaviour {
private int diskID = 0;
private List<DiskModel>[] disks = new List<DiskModel>[3]; //共有三种飞碟
bool[,] diskStatus = new bool[3, 20]; //每种飞碟最多有二十个
int[] size = new int[3]; //保存当前已创建出来的每种飞碟的数量private List<DiskModel> ddisk = new List<DiskModel>();
public void Start() {
for (int i = 0; i < 3; i++) {
size[i] = 0;
disks[i] = new List<DiskModel>();
}
for (int j = 0; j < 3; j++) {
for (int i = 0; i < 20; i++) {
diskStatus[j, i] = true;
}
}
}
//随机获取一种空闲飞碟,如果飞碟不足则新建一个实例
public DiskModel getDisk() {
//随机从三种预制中选择一个飞碟外观
int type = Random.Range(0, 3);
DiskModel disk;
//尝试获取已经被创建但目前处于空闲态的飞碟
for (int i = 0; i < size[type]; i++) {
if (diskStatus[type, i] == false) {
diskStatus[type, i] = true;
disks[type][i].disk.SetActive(true);
disk = disks[type][i];
disk.setDiskID(diskID);
diskID++;
//取出时飞碟不能够有爆炸特效
disk.disk.GetComponent<ParticleSystem>().Stop();
return disk;
}
}
//当前没有可用的空闲飞碟,需要创建
disk = new DiskModel(type, diskID);
diskID++;
disks[type].Add(disk);
diskStatus[type, size[type]] = true;
size[type]++;
disk.disk.GetComponent<ParticleSystem>().Stop();
return disk;
}
//回收飞碟
public void FreeDisk(DiskModel disk) {
int type = disk.getType();
int ID = disk.getDiskID();
for (int i = 0; i < size[type]; i++) {
if (disk.getDiskID() == disks[type][i].getDiskID()) {
diskStatus[type, i] = false;
return;
}
}
}
//根据 gameobject 的 instanceID 来查找飞碟
public DiskModel findDisk(int InstanceID) {
for(int i = 0; i < 3; i++) {
for(int j = 0; j < size[i]; j++) {
if (disks[i][j].disk.GetInstanceID() == InstanceID) return disks[i][j];
}
}
return null;
}
}
2. FlyAction
lyAction的基类 SSAction
在之前的博客已经介绍过。
FlyAction实际上只是一个简单的直线动作,但是与CCMoveAction不同的是其参数是飞行的角度和速度。
public class FlyAction : SSAction{
private Vector3 angle;
private float speed;//根据飞行角度和速度获取一个FlyAction
public static FlyAction GetSSAction(Vector3 angle, float speed) {
FlyAction action = CreateInstance<FlyAction>();
action.angle = angle;
action.speed = speed;
return action;
}
//实现简单的直线飞行
public override void Update() {
transform.position += angle * Time.deltaTime * speed;
}
public override void Start() {
Update();
}}
3. SSActionManager
FlyActionManager 是本次游戏的动作管理者,管理此次控制飞碟运动的多个Action。
FlyActionManager 具有的功能:
- 使用Ruler为每个 Action 设置合适的飞行角度和速度
- 为输入的飞碟寻找一个空闲的 Action 进行飞行动作
- 在每次 Update 时 FlyActionManager 对被使用的所有 action 进行 Update 调用
public class FlyActionManager : SSActionManager, ActionAdapter {
public FlyAction[] actions = new FlyAction[20]; //最多建立20个action
public int[] diskID = new int[20]; //action 所应用的飞碟的ID
public ISceneController controller;
private Ruler ruler; //根据不同 round 对飞碟的性能参数做设置
int round;protected void Start() {
controller = (ISceneController)SSDirector.getInstance().currentSceneController;
controller.actionManager = this;
ruler = new Ruler();
for(int i = 0; i < 20; i++) {
diskID[i] = -1; //开始时没有可用飞碟,ID 均为-1
}
}
public void Update() {
for(int i = 0; i < 20; i++) {
if (diskID[i] != -1) {
//执行有附着在飞碟上的 action
actions[i].Update();
}
}
}
public void setRound(int round) {
this.round = round;
}
public void UFOFly(DiskModel disk) {
ruler.setRound(round);
disk.disk.transform.position = ruler.getStart();//设置飞碟的出现位置
int index = 0;
for (; diskID[index] != -1; index++) ;//找到空闲的 Action
actions[index] = FlyAction.GetSSAction(ruler.getAngle(), ruler.getSpeed());
diskID[index] = disk.getDiskID();
disk.disk.GetComponent<Rigidbody>().useGravity = false;
disk.disk.GetComponent<Rigidbody>().velocity = Vector3.zero;
this.RunAction(disk.disk, actions[index], this);
}
public void freeAction(DiskModel disk) {
for(int i = 0; i < 20; i++) {
//当飞碟不再需要时,actionManager可以简单的将对应的action设为空闲,而非直接删除 action
if(diskID[i] == disk.getDiskID()) {
diskID[i] = -1;
break;
}
}
}}
4. PhyUFOFlyAction
控制飞碟的物理学运动,与运动学类似,但要对游戏对象的rigidbody 进行操作。
public class PhyUFOFlyAction : SSAction {
private Vector3 angle; //飞行角度
float speed; //飞行初速度private PhyUFOFlyAction() { }
public static PhyUFOFlyAction GetSSAction(Vector3 angle, float speed) {
//初始化物体将要运动的初速度向量
PhyUFOFlyAction action = CreateInstance<PhyUFOFlyAction>();
action.angle = angle;
action.speed = speed;
return action;
}
public override void Start() {
//使用重力以及给一个初速度
gameobject.GetComponent<Rigidbody>().velocity = angle * speed;
gameobject.GetComponent<Rigidbody>().useGravity = true;
}}
5. PhyFlyActionManager
public class PhyFlyActionManager : SSActionManager, ActionAdapter {public PhyUFOFlyAction[] actions = new PhyUFOFlyAction[20]; //最多建立20个action
public int[] diskID = new int[20]; //action 所应用的飞碟的ID
public ISceneController controller;
private Ruler ruler; //根据不同 round 对飞碟的性能参数做设置
int round;
protected void Start() {
controller = (ISceneController)SSDirector.getInstance().currentSceneController;
controller.actionManager = this;
ruler = new Ruler();
for (int i = 0; i < 20; i++) {
diskID[i] = -1; //开始时没有可用飞碟,ID 均为-1
}
}
public void setRound(int round) {
this.round = round;
}
public void UFOFly(DiskModel disk) {
ruler.setRound(round);
disk.disk.transform.position = ruler.getStart();//设置飞碟的出现位置
int index = 0;
for (; diskID[index] != -1; index++) ;//找到空闲的 Action
Vector3 angle = Vector3.left;
int flag = Random.Range(0, 2);
if (flag == 1) angle *= -1;
actions[index] = PhyUFOFlyAction.GetSSAction(angle, ruler.getSpeed());//从ruler中获取初速度和飞行角度,加速度为10
diskID[index] = disk.getDiskID();
this.RunAction(disk.disk, actions[index], this);
}
public void freeAction(DiskModel disk) {
disk.disk.GetComponent<Rigidbody>().velocity = Vector3.zero;
disk.disk.GetComponent<Rigidbody>().useGravity = false;
for (int i = 0; i < 20; i++) {
//当飞碟不再需要时,actionManager可以简单的将对应的action设为空闲,而非直接删除 action
if (diskID[i] == disk.getDiskID()) {
diskID[i] = -1;
break;
}
}
}}
6. ISceneController
其主要功能为:
- 在飞碟数量小于当前 round 值规定时,向场景中发送飞碟;
- 对场景中所有飞碟进行位置判断,如果超出视野范围则“销毁”飞碟;
- 判断玩家的射击操作,击中飞碟后进行爆炸特效、增加分数、销毁飞碟等一系列处理;
public class ISceneController : MonoBehaviour, IUserAction {
public ActionAdapter actionManager;
public DiskFactory diskFactory;private List<DiskModel> currentDisks = new List<DiskModel>();
private int diskCount = 0; //当前场景中的飞碟数量
private int[] maxCount = { 3, 5, 8 }; //每个round中飞碟需要维持的数目,数目不足时将发送新的飞碟
private int round = 0; //当前游戏的 round
private int currentTrial = 0;//当前游戏的 trail
private int score = 0; //获得的分数
private int[] scoreOfRound = { 10, 20, 30 }; //每个round需要达成的分数目标
private bool playing = false;
private int life = 100; //血量
public UserGUI userGUI;
void Start() {
SSDirector director = SSDirector.getInstance();
director.currentSceneController = this;
diskFactory = Singleton<DiskFactory>.Instance;
actionManager = gameObject.AddComponent<PhyFlyActionManager>() as ActionAdapter;
actionManager.setRound(round);
userGUI = gameObject.AddComponent<UserGUI>() as UserGUI;
}
void Update() {
//发送飞碟
if (playing) {
if (diskCount < maxCount[round]) {
sendDisk();
}
//检查当前游戏状态是否允许升级
checkStatus();
removeRemoteDisk();
//检查玩家的射击操作
if (Input.GetButtonDown("Fire1")) {
Vector3 mp = Input.mousePosition;
Hit(mp);
}
}
}
public void startGame() {
life = 100;
playing = true;
}
public int getScore() {
return score;
}
public int getLife() {
return life;
}
public int getTrail() {
return currentTrial;
}
public int getRound() {
return round;
}
//检查当前游戏状态
//检查当前trail是否足以进入下一 round
//检查当前round是否足够结束游戏
public void checkStatus() {
//此时的分数大于设置的阈值,游戏进入下一阶段,分数清零重新计算
if (score >= scoreOfRound[round]) {
currentTrial++;
score = 0;
//当游戏的trail大于3时进入下一 round
if (currentTrial >= 3) {
round++;
life = 100;//当游戏进入到新的round生命值回复
if (round >= 3) winGame();
currentTrial = 0;
actionManager.setRound(round);
}
}
}
//判断飞碟是否已经离开视野
private bool outOfSight(Vector3 pos) {
return pos.x > 35 || pos.x < -35
|| pos.y > 35 || pos.y < -35
|| pos.z > 10 || pos.z < -300;
}
//检查当前所有被使用的飞碟是否已经飞出视野
//将飞出视野的飞碟“销毁”
//每销毁一个飞碟就将当前飞碟数量减一,游戏将自动补齐缺少的飞碟
private void removeRemoteDisk() {
for (int i = 0; i < diskCount; i++) {
GameObject tmp = currentDisks[i].disk;
if (outOfSight(tmp.transform.position)) {
tmp.SetActive(false);
diskFactory.FreeDisk(currentDisks[i]);
actionManager.freeAction(currentDisks[i]);
currentDisks.Remove(currentDisks[i]);
diskCount--;
life--;
}
}
}
//发送飞碟
private void sendDisk() {
diskCount++;
DiskModel disk = diskFactory.getDisk(); //从工厂获取新的飞碟
currentDisks.Add(disk); //将新飞碟加入到当前的列表
actionManager.UFOFly(disk); //令飞碟进行移动
}
//检查玩家是否射中飞碟
public void Hit(Vector3 pos) {
Ray ray = Camera.main.ScreenPointToRay(pos);
RaycastHit[] hits;
hits = Physics.RaycastAll(ray);
DiskModel hitDisk;
for (int i = 0; i < hits.Length; i++) {
RaycastHit hit = hits[i];
hitDisk = diskFactory.findDisk(hit.collider.gameObject.GetInstanceID());
//射线打中物体
if (hitDisk != null) {
score += hitDisk.score;
//显示爆炸粒子效果
hitDisk.disk.GetComponent<ParticleSystem>().Play();
//等0.5秒后执行回收飞碟
StartCoroutine(WaitingParticle(0.50f, diskFactory, hitDisk));
break;
}
}
}
public void winGame() {
playing = false;
Debug.Log("you win");
}
//暂停几秒后回收飞碟
IEnumerator WaitingParticle(float wait_time, DiskFactory diskFactory, DiskModel hitDisk) {
yield return new WaitForSeconds(wait_time);
if (hitDisk.disk.active == true) {
hitDisk.disk.SetActive(false);
hitDisk.disk.GetComponent<ParticleSystem>().Stop();
hitDisk.disk.GetComponent<Rigidbody>().velocity = Vector3.zero;
hitDisk.disk.GetComponent<Rigidbody>().useGravity = false;
currentDisks.Remove(hitDisk);
actionManager.freeAction(hitDisk);
diskFactory.FreeDisk(hitDisk);
diskCount--;
}
}}
7. UserGUI
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();
bool show;
int round;
bool changeRound;
bool playing = true;
void Start() {
show = true;
changeRound = false;
action = SSDirector.getInstance().currentSceneController as IUserAction;
}
void OnGUI() {
if (!playing) {
if (action.getLife() < 0) {
GUI.Button(new Rect(0, 0, Screen.width, Screen.width), "YOU LOSE");
}
else {
GUI.Button(new Rect(0, 0, Screen.width, Screen.width), "YOU WIN");
}
return;
}
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;
if (action.getLife() < 0) {
playing = false;
}
if (changeRound) {
GUI.Label(new Rect(Screen.width / 2 - 120, Screen.width / 2 - 220, 400, 100), " N E X T R O U N D ", over_style);
if (GUI.Button(new Rect(0, 0, Screen.width, Screen.width), "press to continue")) {
changeRound = false;
action.startGame();
}
}
else {
if (show) {
GUI.Label(new Rect(Screen.width / 2 - 170, Screen.width / 2 - 180, 400, 100), "大量UFO出现,点击它们即可消灭,快来加入战斗吧", text_style);
if (GUI.Button(new Rect(Screen.width / 2 - 40, Screen.width / 2 - 120, 100, 50), "开始")) {
show = false;
action.startGame();
}
}
else {
GUI.Label(new Rect(Screen.width - 120, 20, 200, 50), "score:", text_style);
GUI.Label(new Rect(Screen.width - 70, 20, 200, 50), action.getScore().ToString(), bold_style);
GUI.Label(new Rect(Screen.width - 120, 50, 200, 50), "trial:", text_style);
GUI.Label(new Rect(Screen.width - 70, 50, 200, 50), (action.getTrail() + 1).ToString(), bold_style);
GUI.Label(new Rect(Screen.width - 120, 80, 50, 50), "life:", text_style);
GUI.Label(new Rect(Screen.width - 70, 80, 50, 50), action.getLife().ToString(), bold_style);
if (action.getRound() > round) {
round = action.getRound();
if (round > 2) playing = false;
changeRound = true;
}
}
}
}}
四、 运行界面与代码传送门