游戏实践
编写一个简单的鼠标打飞碟(Hit UFO)游戏
要求
- 游戏内容要求
- 游戏有 n 个 round,每个 round 都包括10 次 trial;
- 每个 trial 的飞碟的色彩、大小、发射位置、速度、角度、同时出现的个数都可能不同。它们由该 round 的 ruler 控制;
- 每个 trial 的飞碟有随机性,总体难度随 round 上升;
- 鼠标点中得分,得分规则按色彩、大小、速度不同计算,规则可自由设定。
- 游戏的要求:
- 使用带缓存的工厂模式管理不同飞碟的生产与回收,该工厂必须是场景单实例的!具体实现见参考资源 Singleton 模板类
- 尽可能使用MVC 结构实现人机交互与游戏模型分离
工厂使用案例:弹药和敌人:减少,重用和再利用
实践要点
- 学习并掌握在Unity输入信息和信息的处理(包括键盘按键输入、鼠标点击等),在本游戏中是使用鼠标点击飞碟作为输入
- 面向对象编程:使用工厂模式管理对象,实现游戏对象的生产、回收和再利用,从而避免重复地实例化对象,销毁对象,减少内存new与delete时所带来的性能消耗。同时将工厂设为场景单实例的。
游戏结构
与之前的牧师与魔鬼相比,导演类、场记、处理用户交互的UserGUI类的关系不变,动作管理方面也是用的动作管理器来帮助场记实现物体运动的管理,但是需要根据实际需要更改FirstActionmanager的动作函数flyDisk供场记调用。场记的判断功能也同样是分离为一个单独的类,聚合与长场记类FirstController之中,这里要根据游戏的实际规则进行定义。最大的区别是之前场记载加载数据是直接实例化包含游戏对象的控制器,而现在时需要依赖工厂类来实现游戏对象的管理。
具体代码
- 飞碟Disk类:定义飞碟的特征和运动属性运动
public class Disk : MonoBehaviour
{
public float size;//飞碟大小
public Color color;//飞碟颜色
public float angle;//飞碟
public float power;//
}
- 工厂DiskFactory
工厂可以生产、回收、再利用飞碟。工厂获取飞碟时首先应该判断工厂中是否有存在的未利用的飞碟,如果有则直接获取飞碟利用,其次才是生产新的飞碟。当飞碟在游戏场景中不需要时,不销毁它,而是通过飞碟灭活等手段移到玩家看不到的地方。这些飞碟会被工厂保存再等待队列中,直到下一次获取飞碟请求时,再拿出来利用。工厂使用两个队列来分别保存正在使用的飞碟和等待使用的飞碟。被获取后的飞碟加入正在使用的队列,回收的飞碟加入自由客再利用的队列。
public class DiskFactory : MonoBehaviour
{
public GameObject diskPrefab;
private List<GameObject> usingDisks = new List<GameObject>(); //正在使用的飞碟
private List<GameObject> freeDisks = new List<GameObject>(); //使用过已被释放的,可以重复使用
int nameIndex;
private void Awake()
{
diskPrefab = GameObject.Instantiate<GameObject>(Resources.Load<GameObject>("Prefabs/disk"), Vector3.zero, Quaternion.identity);
diskPrefab.name = "prefab";
diskPrefab.SetActive(false);
nameIndex = 0;
}
public GameObject getDisk(int round)
{
GameObject newDisk = null;
if (freeDisks.Count > 0)
{
newDisk = freeDisks[0].gameObject;
freeDisks.Remove(freeDisks[0]);
}
else
{
newDisk = GameObject.Instantiate<GameObject>(diskPrefab, Vector3.zero, Quaternion.identity);
newDisk.AddComponent<Disk>();
newDisk.name = nameIndex.ToString();
nameIndex++;
}
return newDisk;
}
//回收飞碟
public void freeDisk(GameObject usedDisk)
{
if (usedDisk != null)
{
usedDisk.SetActive(false);
usingDisks.Remove(usedDisk);
freeDisks.Add(usedDisk);
}
}
}
- Singleten
场景单实例的使用很简单,只需要将 MonoBehaviour 子类对象挂载任何一个游戏对象上,然后在任意位置使用代码Singleton.Instance 就可以获得该对象。通过Singleten便可以实现工厂为场景单实例。
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;
}
}
}
动作管理类的框架基本不变:
- 动作基类SSAction声明动作共有的属性和方法,被子类继承
- 动作CCSequenceAction继承SSAction实现几个动作的顺序组合,但在本游戏中没有用到
- 动作事件接口SSActionEventType定义事件处理接口,通过这个接口回调,实现动作完成时的通知
- 动作管理基类SSActionManager,实现了所有动作的基本管理,它将需要等待的动作加入等待列表,需要删除的动作加入删除列表,并进行动作的运行和回收。通过RunAction方法把游戏对象与动作绑定,并绑定该动作事件的消息接收者
注:以上代码牧师与魔鬼分离版代码一致,在之前博客中已有具体分析
本次游戏具体的飞碟飞行动作和时间管理器需要具体定义
- 飞碟动作类DiskFlyAction
DiskFlyAction是SSAction的子类,实现了一个具体的动作。动作由初速度、受力大小和角度(决定加速度)来决定。飞碟的位置在update中根据插值得到,从而实现飞碟的移动。通过飞碟位置判断它在屏幕范围外时,结束动作并通知
public class DiskFlyAction : SSAction
{
private Vector3 start_vector; //初速度向量
private Vector3 gravity_vector = Vector3.zero; //加速度的向量,初始时为0
private float time; //已经过去的时间
private Vector3 current_angle = Vector3.zero; //当前时间的欧拉角
private DiskFlyAction() { }
public static DiskFlyAction GetSSAction(float angle, float power)
{
//初始化物体将要运动的初速度向量
DiskFlyAction action = CreateInstance<DiskFlyAction>();
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 = 0; //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.transform.position.y > 10)
{
this.destroy = true;
this.callback.SSActionEvent(this);
}
}
public override void Start() { }
}
- 动作控制器FirstActionManager
为了使FirstController调用起来更加简洁,设计FirstActionManager类,它继承ActionManager,封装了一些函数
public class FirstActionManager : SSActionManager
{
public DiskFlyAction fly; //飞碟飞行的动作
public FirstController scene_controller; //当前场景的场景控制器
protected void Start()
{
scene_controller = (FirstController)Director.GetInstance().currentSceneController;
scene_controller.actionManager = this;
}
//飞碟飞行
public void diskFly(GameObject disk, float angle, float power)
{
fly = DiskFlyAction.GetSSAction(angle, power);
this.RunAction(disk, fly, this);
}
}
- 规则管理器Ruler
由Ruler记录玩家的分数并在击中时进行对应轮数分数的更新。根据轮数设置不同的游戏难度,具体为Round越大,飞碟速度越快、大小越小,飞碟出现的间隔越短,过关要求的分数越高。并且由Ruler来判断玩家能否进入下一轮。
public class Ruler : MonoBehaviour
{
public int[] score;
public Ruler()
{
score = new int[10];
}
public void init()
{
for (int i = 0; i < 10; i++)
score[i] = 0;
}
public void updateScore(int roundIndex)
{
score[roundIndex] += 1;
}
public float setInterval(int round)
{
return (float)(2 - 0.2 * round);
}
public int getTargetThisRound(int round)
{
if (round != -1)
{
return 5 + round > 10 ? 10 : 5 + round;
}
return 0;
}
public bool enterNextRound(int round)
{
if (round != -1 && this.score[round - 1] >= (5 + round > 10 ? 10 : 5 + round))
{
return true;
}
return false;
}
public Vector3 setScale(int round)
{
float x = Random.Range((float)(1 - 0.1 * round), (float)(2 - 0.1 * round));
float y = Random.Range((float)(1 - 0.1 * round), (float)(2 - 0.1 * round));
float z = Random.Range((float)(1 - 0.1 * round), (float)(2 - 0.1 * round));
return new Vector3(x, y, z);
}
public float setPower(int round)
{
if (round == 1)
{
return 2;
}
return 3 * round;
}
}
- 场记FirstController
继承ISceneController,和IUserAction,并实现接口中的定义的函数。包括使用工厂准备每一轮用到的飞碟,抛出前进行飞碟性质的设置,处理用户击中飞碟,重新开始游戏的操作,借助Ruler进行游戏的进度管理等。
public class FirstController : MonoBehaviour, ISceneController, UserAction
{
public int round;
public int trial;
public float interval;
public int score;
private UserGUI userGUI;
private Ruler ruler;
public FirstActionManager actionManager;
public GameObject cam;
private Queue<GameObject> disksQueue = new Queue<GameObject>();
private bool isRestart;
void Awake()
{
Director director = Director.GetInstance();
director.currentSceneController = this;
director.currentSceneController.loadResources();
userGUI = gameObject.AddComponent<UserGUI>() as UserGUI;
actionManager = gameObject.AddComponent<FirstActionManager>() as FirstActionManager;
ruler = gameObject.AddComponent<Ruler>() as Ruler;
cam.transform.position = new Vector3(0,-2,-15);
}
public void loadResources()
{
getDisksForNextRound();
}
public void getDisksForNextRound()
{
DiskFactory diskFactory = Singleton<DiskFactory>.Instance;
int numDisk = 10;
for (int i = 0; i < numDisk; i++)
{
GameObject disk = diskFactory.getDisk(round);
disksQueue.Enqueue(disk);
}
}
// Use this for initialization
void Start()
{
round = 1;
interval = 0;
trial = 0;
loadResources();
userGUI.targetThisRound = ruler.getTargetThisRound(round);
}
// Update is called once per frame
void Update()
{
if (ruler.enterNextRound(round))
{
round++;
trial = 0;
getDisksForNextRound();
userGUI.score = this.score = 0;
userGUI.targetThisRound = ruler.getTargetThisRound(round);
}
else if (!ruler.enterNextRound(round) && trial == 11)
{
round = -1;
}
if (this.round >= 1)
{
if (interval > ruler.setInterval(round))
{
if (trial < 10)
{
throwDisk();
interval = 0;
trial++;
}
else if (trial == 10)
{
trial++;
}
}
else
{
interval += Time.deltaTime;
}
}
userGUI.round = this.round;
}
public void throwDisk()
{
if (disksQueue.Count != 0)
{
GameObject disk = disksQueue.Dequeue();
setDiskProperty(disk, round);
disk.SetActive(true);
actionManager.diskFly(disk, disk.GetComponent<Disk>().angle, disk.GetComponent<Disk>().power);
}
}
public void hit(Vector3 pos)
{
Camera ca;
if (cam != null) ca = cam.GetComponent<Camera>();
else ca = Camera.main;
Ray ray = ca.ScreenPointToRay(Input.mousePosition);
RaycastHit[] hits;
hits = Physics.RaycastAll(ray);
for (int i = 0; i < hits.Length; i++)
{
RaycastHit hit = hits[i];
if (hit.collider.gameObject.GetComponent<Disk>() != null)
{
//Debug.Log("hit");
hit.collider.gameObject.SetActive(false);
userGUI.score += 1;
ruler.updateScore(round - 1);
}
}
}
public void setDiskProperty(GameObject disk, int round)
{
disk.transform.position = this.setRandomInitPos();
disk.GetComponent<Renderer>().material.color = setRandomColor();
disk.GetComponent<Disk>().angle = setRandomAngle();
disk.transform.localScale = ruler.setScale(round);
disk.GetComponent<Disk>().power = ruler.setPower(round);
}
public void restart()
{
Debug.Log("restart");
round = 1;
userGUI.round = 1;
interval = 0;
trial = 0;
ruler.init();
userGUI.score = ruler.score[round];
getDisksForNextRound();
userGUI.targetThisRound = ruler.getTargetThisRound(round);
}
}
- 导演类SSDirector代码和之前的一样
- UserGUI
接受用户输入,并在界面上显示游戏分数,当前轮数和输赢信息
public class UserGUI : MonoBehaviour
{
//...
private void Update()
{
if (Input.GetButtonDown("Fire1"))
{
Debug.Log("Fire1");
Vector3 pos = Input.mousePosition;
action.hit(pos);
}
}
// Update is called once per frame
void OnGUI()
{
GUI.skin.label.font = blue_font;
GUI.Label(new Rect(Screen.width / 2 - 50, 20, 180, 50), " Hit UFO ");
GUI.Label(new Rect(Screen.width / 2 - 30, 50, 180, 50), "score: " + score.ToString());
GUI.Label(new Rect(Screen.width / 2 + 60, 50, 180, 50), "goal: " + targetThisRound.ToString());
if (round != -1)
{
GUI.Label(new Rect(Screen.width / 2 - 120, 50, 100, 50), "Round: " + round.ToString());
}
else if (round == -1)
{
GUI.Label(new Rect(Screen.width / 2 - 120, 50, 100, 50), "You Lose!");
}
if (GUI.Button(new Rect(Screen.width / 2 - 40, 240, 70, 30), "Restart"))
{
action.restart();
}
}
}
游戏效果
在Main Camera挂载FirstController和DiskFactory,运行
演示视频
视频