第五次作业 与游戏世界交互
1. 编写一个简单的鼠标打飞碟(Hit UFO)游戏
- 游戏内容要求
- 游戏有 n 个 round,每个 round 都包括10 次 trial;
- 每个 trial 的飞碟的色彩、大小、发射位置、速度、角度、同时出现的个数都可能不同。它们由该round 的 ruler 控制;
- 每个 trial 的飞碟有随机性,总体难度随 round 上升;
- 鼠标点中得分,得分规则按色彩、大小、速度不同计算,规则可自由设定。
- 游戏的要求
- 使用带缓存的工厂模式管理不同飞碟的生产与回收,该工厂必须是场景单实例的!具体实现见参考资源 Singleton 模板类
- 近可能使用前面 MVC 结构实现人机交互与游戏模型分离
- 实现
游戏规则
- 游戏不限round的次数,每个roung有10次trial;
- 飞碟的颜色有三种(红、绿、蓝),每个 trial 的飞碟的色彩、大小、发射位置、速度、角度、同时出现的个数都可能不同;
- 每个 trial 的飞碟有随机性,总体难度随 round 上升;
- 鼠标点中得分,绿色记1分,红色记2分,蓝色记3分;
游戏看开始有20条生命,击不中掉一条命,掉为0时游戏结束。
(1) 资源目录树
由三部分(Scenes
除外)组成:
Materials
:存放背景图
Resources
:存放预制(飞碟)
Scripts
:存放代码脚本
Resources
:
Scripts
:
(2) 代码实现
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, 1, 1); //飞碟大小
}
DiskFactory.cs
单实例类,用场景单实例创建,使用模板模式根据预制和规则制作飞碟,对象模板包括飞碟对象和飞碟数据。有工厂方法GetDisk
产生飞碟,回收方法FreeDisk
回收飞碟
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class DiskFactory : MonoBehaviour
{
public GameObject UFO_instace = null;
private List<DiskData> used = new List<DiskData>();
private List<DiskData> free = new List<DiskData>();
public GameObject GetDisk(int round)
{
int random = 0;
int range1 = 1, range2 = 4, range3 = 7;
float start_y = -10f;
string tag;
UFO_instace = null;
//根据回合,随机选择要飞出的飞碟
if (round == 1)
{
random = Random.Range(0, range1);
}
else if (round == 2)
{
random = Random.Range(0, range2);
}
else
{
random = Random.Range(0, range3);
}
//将要选择的飞碟的tag
if (random <= range1)
{
tag = "disk1";
}
else if (random <= range2 && random > range1)
{
tag = "disk2";
}
else
{
tag = "disk3";
}
//寻找相同tag的空闲飞碟
for (int i = 0; i < free.Count; i++)
{
if (free[i].tag == tag)
{
UFO_instace = free[i].gameObject;
free.Remove(free[i]);
break;
}
}
//如果空闲列表中没有,则重新实例化飞碟
if (UFO_instace == null)
{
if (tag == "disk1")
{
UFO_instace = Instantiate(Resources.Load<GameObject>("Prefabs/disk1"), new Vector3(0, start_y, 0), Quaternion.identity);
}
else if (tag == "disk2")
{
UFO_instace = Instantiate(Resources.Load<GameObject>("Prefabs/disk2"), new Vector3(0, start_y, 0), Quaternion.identity);
}
else
{
UFO_instace = Instantiate(Resources.Load<GameObject>("Prefabs/disk3"), new Vector3(0, start_y, 0), Quaternion.identity);
}
//给新实例化的飞碟赋予其他属性
float ran_x = Random.Range(-1f, 1f) < 0 ? -1 : 1;
UFO_instace.GetComponent<Renderer>().material.color = UFO_instace.GetComponent<DiskData>().color;
UFO_instace.GetComponent<DiskData>().direction = new Vector3(ran_x, start_y, 0);
UFO_instace.transform.localScale = UFO_instace.GetComponent<DiskData>().scale;
}
//添加到使用列表中
used.Add(UFO_instace.GetComponent<DiskData>());
return UFO_instace;
}
//回收飞碟
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;
}
}
}
}
FirstController.cs
场景控制器,负责加载资源
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class FirstController : MonoBehaviour, ISceneController, IUserAction
{
public FlyActionManager action_manager;
public DiskFactory UFO_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 = 1; //回合
private float speed = 1.5f; //发射一个飞碟的时间间隔
private bool playing_game = false; //游戏中
private bool game_over = false; //游戏结束
private bool game_start = false; //游戏开始
private int score_round2 = 10; //去到第二回合所需分数
private int score_round3 = 25; //去到第三回合所需分数
void Start()
{
SSDirector director = SSDirector.GetInstance();
director.CurrentScenceController = this;
UFO_factory = Singleton<DiskFactory>.Instance;
score_recorder = Singleton<ScoreRecorder>.Instance;
action_manager = gameObject.AddComponent<FlyActionManager>() as FlyActionManager;
user_gui = gameObject.AddComponent<UserGUI>() as UserGUI;
}
void Update()
{
if (game_start)
{
//游戏结束,取消定时发送飞碟
if (game_over)
{
CancelInvoke("LoadResources");
}
//设定一个定时器,发送飞碟,游戏开始
if (!playing_game)
{
user_gui.round = round;
InvokeRepeating("LoadResources", 1f, speed);
playing_game = true;
}
//发送飞碟
SendDisk();
//回合升级
if (score_recorder.score >= score_round2 && round == 1)
{
round = 2;
user_gui.round = round;
speed = speed - 0.6f;
CancelInvoke("LoadResources");
playing_game = false;
}
else if (score_recorder.score >= score_round3 && round == 2)
{
round = 3;
user_gui.round = round;
speed = speed - 0.3f;
CancelInvoke("LoadResources");
playing_game = false;
}
}
}
public void LoadResources()
{
disk_queue.Enqueue(UFO_factory.GetDisk(round));
}
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(10f, 15f);
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)
{
UFO_factory.FreeDisk(disk_notshot[i]);
disk_notshot.Remove(disk_notshot[i]);
user_gui.ReduceBlood();
}
}
}
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);
StartCoroutine(WaitingParticle(0.08f, hit, UFO_factory, hit.collider.gameObject));
break;
}
}
}
public int GetScore()
{
return score_recorder.score;
}
public void ReStart()
{
game_over = false;
playing_game = false;
score_recorder.score = 0;
round = 1;
speed = 2f;
}
public void GameOver()
{
game_over = true;
}
//回收飞碟
IEnumerator WaitingParticle(float wait_time, RaycastHit hit, DiskFactory disk_factory, GameObject obj)
{
yield return new WaitForSeconds(wait_time);
hit.collider.gameObject.transform.position = new Vector3(0, -9, 0);
disk_factory.FreeDisk(obj);
}
public void BeginGame()
{
game_start = true;
}
}
FlyActionManager.cs
飞碟动作管理器
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
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);
}
}
SSAction.cs
动作基类
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;
public ISSActionCallback callback;
protected SSAction() { }
public virtual void Start()
{
throw new System.NotImplementedException();
}
public virtual void Update()
{
throw new System.NotImplementedException();
}
}
SSActionManager.cs
动作管理器
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
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);
}
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)
{
}
}
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;
}
}
UFOFlyAction.cs
飞碟的飞行动作设计
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class UFOFlyAction : SSAction
{
public float gravity = -2;
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;
//如果物体y坐标小于-100,动作就做完了
if (this.transform.position.y < -100)
{
this.destroy = true;
this.callback.SSActionEvent(this);
}
}
public override void Start() { }
}
ScoreRecorder.cs
记分规则
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;
//Debug.Log(score);
}
public void Reset()
{
score = 0;
}
}
SequenceAction.cs
由基本或组合动作组合的类
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class SequenceAction : SSAction, ISSActionCallback
{
public List<SSAction> sequence;
public int repeat = -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()
{
}
}
interface.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public interface ISceneController
{
//加载场景
void LoadResources();
}
public interface IUserAction
{
//用户点击游戏界面
void Hit(Vector3 pos);
//获得分数
int GetScore();
//游戏结束
void GameOver();
//游戏重新开始
void ReStart();
//游戏开始
void BeginGame();
}
public enum SSActionEventType : int { Started, Competeted }
public interface ISSActionCallback
{
void SSActionEvent(SSAction source, SSActionEventType events = SSActionEventType.Competeted,
int intParam = 0, string strParam = null, Object objectParam = null);
}
UserGUI.cs
界面设计
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class UserGUI : MonoBehaviour
{
private IUserAction action;
public int round;
public int life = 20; //血量
//每个GUI的style
GUIStyle bold_style = new GUIStyle();
GUIStyle score_style = new GUIStyle();
GUIStyle text_style = new GUIStyle();
GUIStyle description_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()
{
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;
description_style.fontSize = 25;
description_style.normal.textColor = new Color(1, 0, 0);
if (game_start)
{
//用户射击
if (Input.GetButtonDown("Fire1"))
{
Vector3 pos = Input.mousePosition;
action.Hit(pos);
}
GUI.Label(new Rect(10, 5, 200, 30), "Round:", text_style);
GUI.Label(new Rect(65, 5, 200, 30), round.ToString(), score_style);
GUI.Label(new Rect(10, 45, 200, 30), "分数:", text_style);
GUI.Label(new Rect(55, 45, 200, 30), action.GetScore().ToString(), score_style);
GUI.Label(new Rect(10, 25, 200, 30), "生命:", text_style);
GUI.Label(new Rect(55, 25, 200, 30), life.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), "重新开始"))
{
life = 20;
action.ReStart();
return;
}
action.GameOver();
}
}
else
{
GUI.Label(new Rect(Screen.width / 2 - 30, Screen.width / 2 - 350, 100, 100), "Hit UFO!", over_style);
// GUI.Label(new Rect(Screen.width / 2 - 150, Screen.width / 2 - 220, 400, 100), "点击出现的UFO即可销毁,加油!", description_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--;
}
}
Singleton.cs
本次作业限定了创建对象的模式是单例模式,即某个类只能生成一个实例,该类提供了一个全局访问点供外部获取该实例。以下是单例模式的实现。
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) 运行游戏
在完成上述步骤后,将DiskData.cs
挂载到预制的三个飞碟中,分别修改Score
, Color
, Direction
, Scale
的值,然后将DiskFactory.cs
,FirstController.cs
,FlyActionManager.cs
,SSActionManager.cs
,Scorerecorder.cs
和UserGUI.cs
挂载到Main Camera中,并将DiskFactory.cs
的UFO_instance改为Main Camera
,点击运行,显示以下游戏画面:
点击游戏开始
按钮即可开始游戏。
(4)项目代码
2. 编写一个简单的自定义component(选做)
在上面的鼠标打飞碟游戏中,其中 DiskData.cs
就是一个简单的自定义component:
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, 1, 1); //飞碟大小
}
挂载到飞碟上之后,可以在飞碟的Inspector里修改以下属性: