1、编写一个简单的鼠标打飞碟(Hit UFO)游戏
- 游戏内容要求:
- 游戏有 n 个 round,每个 round 都包括10 次 trial;
- 每个 trial 的飞碟的色彩、大小、发射位置、速度、角度、同时出现的个数都可能不同。它们由该 round 的 ruler 控制;
- 每个 trial 的飞碟有随机性,总体难度随 round 上升;
- 鼠标点中得分,得分规则按色彩、大小、速度不同计算,规则可自由设定。
- 游戏的要求:
- 使用带缓存的工厂模式管理不同飞碟的生产与回收,该工厂必须是场景单实例的!具体实现见参考资源 Singleton 模板类
- 近可能使用前面 MVC 结构实现人机交互与游戏模型分离
如果你的使用工厂有疑问,参考:弹药和敌人:减少,重用和再利用;参考:Unity对象池(Object Pooling)理解与简单应用代码质量较低,比较凌乱
2、编写一个简单的自定义 Component (选做)
- 用自定义组件定义几种飞碟,做成预制
- 参考官方脚本手册https://docs.unity3d.com/ScriptReference/Editor.html
- 实现自定义组件,编辑并赋予飞碟一些属性
如果你想了解跟多自定义插件或编辑器的话题,请参考:Unity3d自定义一个编辑器组件/插件的简易教程
游戏规则:
- 一共有三个回合。第一个回合获得5分可以进入第二个回合,第二个回合获得15分可以进入第三个回合,第三回合获得30分就获胜。每个回合的飞碟速度依次增加,飞碟发射的间隔时间依次减少。每回合生命值会恢复初始值。
- 击中飞碟加上相应分数,错过飞碟扣除生命值,当生命值降为0时,游戏失败。每个回合有10次trial,如果10次trial结束,没有进入下一回合,游戏失败。游戏失败会跳出restart按键,游戏界面右上角也有restart按键,点击可以重新开始游戏。
- 游戏界面右上角有pause按键,点击可以暂停发射飞碟,并且页面中间出现continue按键,点击continue按键可以继续游戏。
游戏效果:
项目说明:
- 创建三个飞碟预设,挂载上DiskData,设置score、scale、color。这就是第二道选做题实现的自定义组件。
- 新建一个空对象,挂载上Controllor和DiskFactory。
代码:https://github.com/loudax/3d-game/tree/master/hw5-FlyingSaucer
下面对代码进行说明。
- Actions.cs
SSAction、SSActionManager类,SSActionEventType,ISSActionCallback接口都和之前的牧师与魔鬼都是相同的,直接复用就好。
(1) FlyAction类:实现了SSAction类,定义了飞碟飞行的速度、轨迹和回收条件等。
public class FlyAction : SSAction
{
public float gravity = -0.1f; //飞碟飞行时受重力
public bool go = true; //用于表示是否暂停,go为false时表示游戏暂停
private float time;
private Vector3 start_vector;
private Vector3 gravity_vector = Vector3.zero;
private Vector3 current_angle = Vector3.zero;
private FlyAction()
{
}
public static FlyAction GetSSAction(Vector3 direction, float angle, float power, bool go_)
{
FlyAction action = CreateInstance<FlyAction>();
action.go = go_;
action.start_vector = Quaternion.Euler(new Vector3(0, 0, angle)) * Vector3.right * power;
return action;
}
public override void Update()
{
if(go){
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.x > 20){ //当x坐标大于20时,回收飞碟
this.destroy = true;
this.callback.SSActionEvent(this);
}
}
public override void Start()
{
}
}
(2)FlyActionManager类:定义了飞碟的飞行。
public class FlyActionManager : SSActionManager
{
public FlyAction fly;
public Controllor scene;
protected void Start()
{
scene = (Controllor)SSDirector.GetInstance ().CurrentSceneControllor;
scene.fam = this;
}
public void UFOfly(GameObject disk, float angle, float power, bool go)
{
fly = FlyAction.GetSSAction (disk.GetComponent<DiskData> ().direction, angle, power, go);
this.RunAction (disk, fly, this);
}
}
- DiskFactory.cs
工厂用于生产和回收飞碟。使用飞碟时先在空闲list里找,如果有,就可以放到使用list中然后使用,如果没有空闲飞碟,就新建一个。飞碟使用完毕后,就放回空闲list中。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
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){//获得一个飞碟
string tag;
disk_prefab = null;
if (round == 1){
tag = "disk1";;
}
else if(round == 2){
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){
float start_y = Random.Range(1f, 4f);
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);
}
disk_prefab.GetComponent<MeshRenderer> ().material.color = disk_prefab.GetComponent<DiskData>().color;
disk_prefab.GetComponent<DiskData>().direction = new Vector3(1.0f, start_y, 0);
disk_prefab.GetComponent<DiskData> ().tag = tag;
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;
}
}
}
}
- ScoreRecorder.cs
用于计算得分。
public class ScoreRecorder : MonoBehaviour
{
public int score = 0;
void Start()
{
score = 0;
}
public void Record(GameObject disk)
{
score = score + disk.GetComponent<DiskData>().score;
}
}
- Controllor.cs
用于控制游戏。
public class Controllor : MonoBehaviour,ISceneControllor,IUserAction
{
public FlyActionManager fam;
public DiskFactory df;
public UserGUI ug;
public ScoreRecorder sr;
public RoundControllor rc;
private Queue<GameObject> dq = new Queue<GameObject> ();
private List<GameObject> dfree = new List<GameObject> ();
private int round = 1;
private int trail = 0;
private float t = 1;
private float speed = 2; //飞碟发射间隔时间
void Start(){
SSDirector director = SSDirector.GetInstance();
director.CurrentSceneControllor = this;
df = Singleton<DiskFactory>.Instance;
sr = gameObject.AddComponent<ScoreRecorder> () as ScoreRecorder;
fam = gameObject.AddComponent<FlyActionManager>() as FlyActionManager;
ug = gameObject.AddComponent<UserGUI>() as UserGUI;
rc = gameObject.AddComponent<RoundControllor> () as RoundControllor;
t = speed;
}
void Update ()
{
if(ug.go){
t-=Time.deltaTime;
if(trail > 10){ //超过10次trial后,判为游戏失败
ug.life = 0;
}
if (t < 0) { //发射一个飞碟
LoadResources ();
SendDisk ();
t = speed;
trail++;
}
if ((sr.score >= 5 && round == 1) || (sr.score >= 15 && round == 2) || (sr.score >= 30 && round == 3)) { //判断是否进入下一轮
round++;
trail = 0;
rc.loadRoundData (round);
ug.RecoverBlood();
}
}
}
public void setting(float speed_)
{
speed = speed_;
}
public void LoadResources()
{
dq.Enqueue(df.GetDisk(round));
}
private void SendDisk() //发射飞碟
{
float position_x = 16;
if (dq.Count != 0)
{
GameObject disk = dq.Dequeue();
dfree.Add(disk);
disk.SetActive(true);
float ran_y = Random.Range(1f, 4f);
disk.GetComponent<DiskData>().direction = new Vector3(1f, 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(1f * round, 2f * round);
float angle = Random.Range(0f, 5f);
fam.UFOfly(disk,angle,power,ug.go);
}
for (int i = 0; i < dfree.Count; i++)
{
GameObject temp = dfree[i];
if (temp.transform.position.x > 20 && temp.gameObject.activeSelf == true)
{
df.FreeDisk(dfree[i]);
dfree.Remove(dfree[i]);
ug.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 < dfree.Count; j++)
{
if (hit.collider.gameObject.GetInstanceID() == dfree[j].gameObject.GetInstanceID())
{
not_hit = true;
}
}
if(!not_hit)
{
return;
}
dfree.Remove(hit.collider.gameObject);
sr.Record(hit.collider.gameObject);
hit.collider.gameObject.transform.position = new Vector3(0, -100, 0);
df.FreeDisk(hit.collider.gameObject);
break;
}
}
}
public void Restart (){ //重启游戏
SceneManager.LoadScene(0);
}
public int GetScore (){ //得到得分
return sr.score;
}
public int GetRound(){ //得到轮次
return round;
}
public int GetTrail(){
return trail;
}
}
- RoundControllor.cs
用于控制游戏轮次,主要是控制每一轮的飞碟发射间隔时间。
public class RoundControllor : MonoBehaviour
{
private IUserAction action;
private float speed; //飞碟发射间隔时间
void Start()
{
action = SSDirector.GetInstance().CurrentSceneControllor as IUserAction;
speed = 2.0f;
}
public void loadRoundData(int round)
{
switch (round){
case 1:
break;
case 2:
speed = 1.5f;
action.setting (speed);
break;
case 3:
speed = 1.0f;
action.setting (speed);
break;
}
}
}
- SSDirector.cs
和之前牧师与魔鬼的SSDirector一样。
- Singletion.cs
和课程网站上给出的一样。
- interface.cs
给出一些函数接口,在Controllor.cs中进行实现。
public interface ISceneControllor
{
void LoadResources ();
}
public interface IUserAction
{
void Hit(Vector3 pos);
void Restart();
int GetScore();
void setting(float speed);
int GetRound();
int GetTrail();
}
- UserGUI.cs
public class UserGUI : MonoBehaviour
{
private IUserAction action;
public int life = 6;
public bool go = true;
void Start ()
{
action = SSDirector.GetInstance().CurrentSceneControllor as IUserAction;
}
void OnGUI ()
{
if (Input.GetButtonDown("Fire1")){
Vector3 pos = Input.mousePosition;
action.Hit(pos);
}
GUI.Label(new Rect(20, 10, 200, 50), "score:");
GUI.Label(new Rect(60, 10, 200, 50), action.GetScore().ToString());
GUI.Label(new Rect(20, 30, 50, 50), "hp:");
for (int i = 0; i < life; i++){
GUI.Label(new Rect(40 + 10 * i, 30, 50, 50), "X");
}
GUI.Label(new Rect(20, 50, 50, 50), "trail:");
GUI.Label(new Rect(60, 50, 50, 50), action.GetTrail().ToString());
if(action.GetRound() == 1){
GUI.Label(new Rect(200, 10, 200, 50), "ROUND");
GUI.Label(new Rect(250, 10, 200, 50), action.GetRound().ToString());
GUI.Label(new Rect(200, 30, 300, 50), "分数到达5分进入下一关,打中一个飞碟得1分,错过一个飞碟生命值减1,生命值归零时游戏失败。");
}
else if(action.GetRound() == 2){
GUI.Label(new Rect(200, 10, 200, 50), "ROUND");
GUI.Label(new Rect(250, 10, 200, 50), action.GetRound().ToString());
GUI.Label(new Rect(200, 30, 300, 50), "分数到达15分进入下一关,打中一个飞碟得2分,错过一个飞碟生命值减1,生命值归零时游戏失败。");
}
else if(action.GetRound() == 3){
GUI.Label(new Rect(200, 10, 200, 50), "ROUND");
GUI.Label(new Rect(250, 10, 200, 50), action.GetRound().ToString());
GUI.Label(new Rect(200, 30, 300, 50), "分数到达30分获得胜利,打中一个飞碟得3分,错过一个飞碟生命值减1,生命值归零时游戏失败。");
}
else{
GUI.Label(new Rect(Screen.width / 2 - 50, Screen.width / 2 - 300, 100, 100), "You Win!");
}
if(GUI.Button(new Rect(550, 10, 100, 40), "Pause")){
go = false;
}
if(go == false){
if(GUI.Button(new Rect(Screen.width / 2 - 60, Screen.width / 2 - 250, 100, 40), "Continue")){
go = true;
}
}
if (GUI.Button(new Rect(670, 10, 100, 40), "Restart")){
action.Restart();
return;
}
if (life == 0){
GUI.Label(new Rect(Screen.width / 2 - 50, Screen.width / 2 - 300, 100, 100), "Game Over!");
if (GUI.Button(new Rect(Screen.width / 2 - 60, Screen.width / 2 - 250, 100, 40), "Restart")){
action.Restart();
return;
}
}
}
public void ReduceBlood() //生命值减1
{
if(life > 0)
life--;
}
public void RecoverBlood() //生命值恢复初始状态
{
life = 6;
}
}
- DiskData.cs
定义了飞碟的一些变量,包括分数、颜色、方向、大小、tag(属于第几回合)。
public class DiskData : MonoBehaviour
{
public int score = 1;
public Color color = Color.white;
public Vector3 direction;
public Vector3 scale = new Vector3( 1 ,0.1f, 1);
public string tag;
}
- DiskEditor.cs
实现自定义组件,给飞碟赋予分数、颜色、大小的属性。
public class DiskEditor : Editor
{
SerializedProperty score;
SerializedProperty color;
SerializedProperty scale;
void OnEnable()
{
score = serializedObject.FindProperty("score");
color = serializedObject.FindProperty("color");
scale = serializedObject.FindProperty("scale");
}
public override void OnInspectorGUI()
{
serializedObject.Update();
EditorGUILayout.IntSlider(score, 0, 5, new GUIContent("score"));
EditorGUILayout.PropertyField(color);
EditorGUILayout.PropertyField(scale);
serializedObject.ApplyModifiedProperties();
}
}