1: 任务概述
本次编程实践需要设计一个鼠标打地鼠的游戏:
- 将游戏设置为多个round,每个round有n个trail。每个 trail 的飞碟的色彩,大小;
- 发射位置,速度,角度,每次发射飞碟数量不一;。
- 鼠标击中得分,得分按色彩、大小、速度不同计算,计分规则自由定
2:项目结构:
本次编程依旧使用MVC结构,游戏由导演、场记、运动管理员和演员等部分构成。为了管理飞碟的创建于销毁,以及发放和使用这些飞碟,使用了工厂方法。具体的结构如下:
-
记分员按飞碟的数据计分,记分员拥有计分规则
-
场记只需要管理出飞碟规则与管理碰撞就可以了
-
DiskFactory 类是一个单实例类,用前面场景单实例创建
-
DiskFactory 类有工厂方法 GetDisk 产生飞碟,有回收方法 Free(Disk)
-
DiskFactory 使用模板模式根据预制和规则制作飞碟
-
对象模板包括飞碟对象与飞碟数据
3:项目代码:
3.1:管理飞碟信息DiskData.cs:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class DiskData : MonoBehaviour{
public int score = 1; //射击此飞碟得分
public Color color = Color.white; //飞碟颜色
public Vector3 direction; //飞碟初始的位置
public Vector3 scale = new Vector3( 1 ,0.25f, 1); //飞碟大小
}
3.2 导演:Director
用于实例化一个导演类
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;
}
}
3.3 工厂模式: DiskFactors
本次实验要求使用带缓存的工厂模式来管理飞碟的发放与回收利用。如果需要使用一个飞碟示例,但是如果仓库中没有该飞碟实例,就需要创建一个新的实例,在飞碟使用过后,工厂不会销毁该飞碟实例,而是将飞碟回收到空闲队列,等待下一次的调用 。
同时工厂需要使用单例模式。
工厂使用两个链表来储存飞碟。分别代表使用的飞碟队列和空闲的飞碟队列。
public GameObject disk_prefab = null; //飞碟预制体
private List<DiskData> used = new List<DiskData>(); //正在被使用的飞碟列表
private List<DiskData> free = new List<DiskData>(); //空闲的飞碟列表
工厂主要有两个方法,分别用于获取空闲的实例和让使用中的实例返回到空闲的状态。
1:获取空闲飞碟实例,参数是当前的round值,round 1:只会获取disk1 类型
round 2: 可以获取disk1 和 disk2 两种类型
round 3: 可以获取disk1 和 disk2 和disk 3三种类型值。
随机选择一个类型,并从队列中寻找该类型的飞碟,如果找到一个实例,那么就从空闲队列中选出该实例,如果没有该类型的实例,就从预制中创建一个新实例。
最后需要对飞碟的位置等属性进行设置。
public GameObject GetDisk(int round){
int choice = 0;
int scope1 = 1, scope2 = 4, scope3 = 7; //随机的范围
float start_y = -10f; //刚实例化时的飞碟的竖直位置
string name;
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);
//将要选择的飞碟的name
if(choice <= scope1) name = "disk1";
else if(choice <= scope2 && choice > scope1) name = "disk2";
else name = "disk3";
//寻找相同name的空闲飞碟
for(int i=0;i<free.Count;i++) if(free[i].name == name){
disk_prefab = free[i].gameObject;
free.Remove(free[i]);
break;
}
//如果空闲列表中没有,则重新实例化飞碟
if(disk_prefab == null){
if (name == "disk1") disk_prefab = Instantiate(Resources.Load<GameObject>("Prefabs/disk1"), new Vector3(0, start_y, 0), Quaternion.identity);
else if (name == "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;
}
2:从使用队列删除一个实例,并将该实例回收到空闲队列中:
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;
}
}
3.4 单实例模式 Singleton
在该项目中,工厂和分数计数员使用的都是单实例模式。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
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;
}
}
}
3.5 场记 FirstController
1:成员变量:
public FlyActionManager action_manager; //动作管理
public DiskFactory disk_factory; //飞碟工厂
public UserGUI user_gui; //游戏交互界面
public ScoreRecorder score_recorder; //分数计算
private Queue<GameObject> disk_queue = new Queue<GameObject>(); //游戏场景中的飞碟队列
private List<GameObject> disk_notshot = new List<GameObject>(); //没有被打中的飞碟队列
private int round = 0; //回合
private float speed = 2f; //发射一个飞碟的时间间隔
private bool playing_game = false; //游戏中
private bool game_over = false; //游戏结束
private bool game_start = false; //游戏开始
private int[][] trial; //记录分数轨迹
private int[] score_round; //每一round的分数
2:初始化 Start
初始化分数变化数组trial[],以及获取工厂、导演等实例。
void Start (){
//初始化参数 一共3级 1级10层
trial = new int[3][];
score_round = new int[3];
int tmp=0;
for (int i=0;i<3;i++){
trial[i]= new int[10];
for (int j=0;j<10;j++){
trial[i][j]=tmp;
tmp+=2*(i+1);
}
score_round[i]=trial[i][0];
}
SSDirector director = SSDirector.GetInstance();
director.CurrentScenceController = this;
disk_factory = Singleton<DiskFactory>.Instance;
score_recorder = Singleton<ScoreRecorder>.Instance;
action_manager = gameObject.AddComponent<FlyActionManager>() as FlyActionManager;
user_gui = gameObject.AddComponent<UserGUI>() as UserGUI;
}
3: Update
在一定的时间间隔会放飞飞碟,同时记录分数,依据当前的分数更新Round,以及飞碟发放的速度:
void Update (){
if(game_start){
//游戏结束,取消定时发送飞碟
if (game_over) CancelInvoke("LoadResources");
//设定一个定时器,发送飞碟,游戏开始
if (!playing_game){
InvokeRepeating("LoadResources", 1f, speed);
playing_game = true;
}
//发送飞碟
SendDisk();
//回合升级
if (score_recorder.score >= score_round[1] && round == 0){
round = 1;
//缩小飞碟发送间隔
speed = speed - 0.6f;
CancelInvoke("LoadResources");
playing_game = false;
}
else if (score_recorder.score >= score_round[2] && round == 1){
round = 2;
speed = speed - 0.5f;
CancelInvoke("LoadResources");
playing_game = false;
}
}
}
4.发射飞碟 sendDisk:
从队列中获取飞碟,并设置为可见,在开始运动前,随机设置运动的力和运动的方向,判断飞行过程是否被击中,修改玩家血量。
private void SendDisk(){
float position_x = 16;
if (disk_queue.Count != 0){
GameObject disk = disk_queue.Dequeue();
disk_notshot.Add(disk);
disk.SetActive(true);
//设置被隐藏了或是新建的飞碟的位置
float ran_y = Random.Range(1f, 4f);
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(5f, 10f);
float angle = Random.Range(15f, 28f);
action_manager.UFOFly(disk,angle,power);
}
for (int i = 0; i < disk_notshot.Count; i++){
GameObject temp = disk_notshot[i];
//飞碟飞出摄像机视野也没被打中
if (temp.transform.position.y < -10 && temp.gameObject.activeSelf == true){
disk_factory.FreeDisk(disk_notshot[i]);
disk_notshot.Remove(disk_notshot[i]);
//玩家血量-1
user_gui.ReduceBlood();
}
}
}
5 记录打击 Hit
判断飞碟是否被击中。记录分数记忆显示爆炸的效果,同时需要回收飞碟。
public void Hit(Vector3 pos){
Ray ray = Camera.main.ScreenPointToRay(pos);
RaycastHit[] hits;
hits = Physics.RaycastAll(ray);
bool not_hit = false;
for (int i = 0; i < hits.Length; i++){
RaycastHit hit = hits[i];
//射线打中物体
if (hit.collider.gameObject.GetComponent<DiskData>() != null){
//射中的物体要在没有打中的飞碟列表中
for (int j = 0; j < disk_notshot.Count; j++)
if (hit.collider.gameObject.GetInstanceID() == disk_notshot[j].gameObject.GetInstanceID()) not_hit = true;
if(!not_hit) return;
disk_notshot.Remove(hit.collider.gameObject);
//记分员记录分数
score_recorder.Record(hit.collider.gameObject);
//显示爆炸粒子效果
Transform explode = hit.collider.gameObject.transform.GetChild(0);
explode.GetComponent<ParticleSystem>().Play();
//执行回收飞碟
StartCoroutine(WaitingParticle(0.02f, hit, disk_factory, hit.collider.gameObject));
break;
}
}
}
3.6 分数记录 ScoreRecorder
分数记录也是一个单实例对象:记录当前玩家的得分:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class ScoreRecorder : MonoBehaviour{
public int score; //分数
void Start (){
score = 0;
}
//记录分数
public void Record(GameObject disk){
int temp = disk.GetComponent<DiskData>().score;
score = temp + score;
}
//重置分数
public void Reset(){
score = 0;
}
}
3.7 交互界面:UserGUI
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class UserGUI : MonoBehaviour{
private IUserAction action;
const int mxlife=10;
public int life = mxlife; //血量
//每个GUI的style
GUIStyle bold_style = new GUIStyle();
GUIStyle score_style = new GUIStyle();
GUIStyle text_style = new GUIStyle();
GUIStyle over_style = new GUIStyle();
private int high_score = 0; //最高分
private bool game_start = false; //游戏开始
void Start (){
action = SSDirector.GetInstance().CurrentScenceController 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;
score_style.normal.textColor = new Color(1,0,1,1);
score_style.fontSize = 16;
over_style.normal.textColor = new Color(1, 0, 0);
over_style.fontSize = 25;
if (game_start){
//用户射击
if (Input.GetButtonDown("Fire1")){
Vector3 pos = Input.mousePosition;
action.Hit(pos);
}
GUI.Label(new Rect(10, 5, 200, 50), "分数:", text_style);
GUI.Label(new Rect(55, 5, 200, 50), action.GetScore().ToString(), score_style);
GUI.Label(new Rect(10, 35, 200, 50), "Round:", text_style);
GUI.Label(new Rect(65, 35, 200, 50), action.getRound().ToString(), score_style);
GUI.Label(new Rect(400, 5, 50, 50), "生命:", text_style);
//显示当前血量
GUI.Label(new Rect(460, 5, 50, 50), getLife().ToString(), score_style);
//游戏结束
if (life == 0){
high_score = high_score > action.GetScore() ? high_score : action.GetScore();
GUI.Label(new Rect(Screen.width / 2 - 20, Screen.width / 2 - 250, 100, 100), "游戏结束", over_style);
GUI.Label(new Rect(Screen.width / 2 - 10, Screen.width / 2 - 200, 50, 50), "最高分:", text_style);
GUI.Label(new Rect(Screen.width / 2 + 50, Screen.width / 2 - 200, 50, 50), high_score.ToString(), text_style);
if (GUI.Button(new Rect(Screen.width / 2 - 20, Screen.width / 2 - 150, 100, 50), "重新开始")){
action.ReStart();
life = mxlife;
return;
}
action.GameOver();
}
}
else{
GUI.Label(new Rect(Screen.width / 2 - 5, Screen.width / 2 - 350, 100, 100), "UFO", over_style);
if (GUI.Button(new Rect(Screen.width / 2 - 20, Screen.width / 2-150, 100, 50), "游戏开始")){
game_start = true;
action.BeginGame();
}
}
}
public void ReduceBlood(){
if(life > 0) life--;
}
public int getLife(){
return life;
}
}
3.8 飞碟动作:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class SSAction : ScriptableObject{
public bool enable = true; //是否正在进行此动作
public bool destroy = false; //是否需要被销毁
public GameObject gameobject; //动作对象
public Transform transform; //动作对象的transform
public ISSActionCallback callback; //动作完成后的消息通知者
protected SSAction() { }
//子类可以使用下面这两个函数
public virtual void Start(){
throw new System.NotImplementedException();
}
public virtual void Update(){
throw new System.NotImplementedException();
}
}
public class SSActionManager : MonoBehaviour, ISSActionCallback{
private Dictionary<int, SSAction> actions = new Dictionary<int, SSAction>(); //将执行的动作的字典集合
private List<SSAction> waitingAdd = new List<SSAction>(); //等待去执行的动作列表
private List<int> waitingDelete = new List<int>(); //等待删除的动作的key
protected void Update(){
foreach (SSAction ac in waitingAdd)
actions[ac.GetInstanceID()] = ac;
waitingAdd.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();
}
foreach (int key in waitingDelete){
SSAction ac = actions[key];
actions.Remove(key);
// DestroyObject(ac);
Object.Destroy(ac);
}
waitingDelete.Clear();
}
public void RunAction(GameObject gameobject, SSAction action, ISSActionCallback manager){
action.gameobject = gameobject;
action.transform = gameobject.transform;
action.callback = manager;
waitingAdd.Add(action);
action.Start();
}
public void SSActionEvent(SSAction source, SSActionEventType events = SSActionEventType.Competeted,
int intParam = 0, string strParam = null, Object objectParam = null){ }
}
public class SequenceAction : SSAction, ISSActionCallback{
public List<SSAction> sequence; //动作的列表
public int repeat = -1; //-1就是无限循环做组合中的动作
public int start = 0; //当前做的动作的索引
public static SequenceAction GetSSAcition(int repeat, int start, List<SSAction> sequence){
SequenceAction action = ScriptableObject.CreateInstance<SequenceAction>();
action.repeat = repeat;
action.sequence = sequence;
action.start = start;
return action;
}
public override void Update(){
if (sequence.Count == 0) return;
if (start < sequence.Count) sequence[start].Update();
}
public void SSActionEvent(SSAction source, SSActionEventType events = SSActionEventType.Competeted,
int intParam = 0, string strParam = null, Object objectParam = null){
source.destroy = false;
this.start++;
if (this.start >= sequence.Count){
this.start = 0;
if (repeat > 0) repeat--;
if (repeat == 0){
this.destroy = true;
this.callback.SSActionEvent(this);
}
}
}
public override void Start(){
foreach (SSAction action in sequence){
action.gameobject = this.gameobject;
action.transform = this.transform;
action.callback = this;
action.Start();
}
}
void OnDestroy(){ }
}
public class UFOFlyAction : SSAction{
public float gravity = -2; //向下的加速度
private Vector3 start_vector; //初速度向量
private Vector3 gravity_vector = Vector3.zero; //加速度的向量,初始时为0
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(){
//计算物体的向下的速度,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.destroy = true;
this.callback.SSActionEvent(this);
}
}
public override void Start() { }
}
public class FlyActionManager : SSActionManager{
public UFOFlyAction fly; //飞碟飞行的动作
public FirstController scene_controller; //当前场景的场景控制器
protected void Start(){
scene_controller = (FirstController)SSDirector.GetInstance().CurrentScenceController;
scene_controller.action_manager = this;
}
//飞碟飞行
public void UFOFly(GameObject disk, float angle, float power){
fly = UFOFlyAction.GetSSAction(disk.GetComponent<DiskData>().direction, angle, power);
this.RunAction(disk, fly, this);
}
}
4:项目文件:
5:运行演示:
玩法介绍: 游戏会随机发射飞碟,如果鼠标击中分数就会叠加,随着分数的增加,飞碟发射的频率会逐渐增加,且分数到达某个值,round会上升,round每次增加,飞碟会增加一种类型,新的类型会更难点击,同时点击新类型的飞碟分数也会更高。一开始玩家的生命值为10。每次错失飞碟都会减少生命值,生命值小于0时,游戏结束:
Disk