游戏实践
Unity“鼠标打飞碟”小游戏(Hit UFO)
游戏规则:
- 游戏包括n个round,每个round有10次trail;
- 玩家通过按下空格键开始每个trail,同时发射飞碟,每个trail的飞碟的色彩、大小、发射位置,速度,角度、同时出现的个数都可能不同,即随机生成;
- 每个trail的飞碟有随机性,总体难度随round上升;
- 玩家在每一个trail都必须在飞碟落地前击毙所有UFO才能成功进入下一个trail,成功通过10个trail时进入下一轮;
- 鼠标每击中一个飞碟加100分,无法通过trail时分数清零。
游戏要求
- 使用带缓存的工厂模式管理不同飞碟的生产与回收,该工厂必须是场景单实例的!具体实现见参考资源Singleton模板类;
- 尽可能使用前面MVC结构实现人机交互与游戏模型分离。
该游戏的UML图如下:
设计模式解读:
- UFOFactory类是一个单实例类,用前面场景单实例创建;
- UFOFactory类有工厂方法getUFO()产生飞碟,有回收方法RecyclingUFO(ufoObject);
- UFOFactory使用模板模式根据预制和规则制作飞碟;
·对象模板包括飞碟对象与飞碟数据。
该游戏的对象池实现的伪代码如下:
具体实现
首先通过create > Material
创建飞碟及其发射器的颜色材料如下:
然后通过create > Prefab
创建预制,包括飞碟、飞碟发射器、背景、爆炸效果,以及文字提示。如下:
各预制的属性设置如下:
接着编写代码。
SceneController
场记SceneController采用单例模式,当其他地方调用getInstance()
时获取到的是同一个SceneController对象。场记SceneController可以调用“记分员”StatusController
和飞碟管理者DiskController
,当玩家按下空格键时SceneController调用launchUFO()
发射飞碟,然后由记分员负责根据玩家点击行为计分(addScore()
/subScore()
)。SceneController代码如下:
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace CarolSum.com
{
public interface IUserAction
{
void launchUFO();
void hitUFO(Vector3 mousePos);
}
public interface IGameStatusOp
{
int getRoundNum();
void addScore();
void subScore();
}
public class SceneController : System.Object, IUserAction, IGameStatusOp
{
private static SceneController instance; //单例模式
private UFOController myUFOCtrl; //管理发射飞碟及点击飞碟
private StatusController myStatusCtrl; //记分员
public static SceneController getInstance()
{
if (instance == null) instance = new SceneController();
return instance;
}
internal void setUFOController(UFOController ufoCtrl)
{
if (myUFOCtrl == null) myUFOCtrl = ufoCtrl;
}
internal void setStatusController(StatusController gameStatus)
{
if (myStatusCtrl == null) myStatusCtrl = gameStatus;
}
internal int getTrailNum()
{
return myStatusCtrl.getTrialNum();
}
//飞碟管家的任务
public void launchUFO()
{
//每次发射之前清0分数
myStatusCtrl.resetScore();
myUFOCtrl.launchUFO();
}
public void hitUFO(Vector3 mousePos)
{
myUFOCtrl.hitUFO(mousePos);
}
//记分员负责的工作
public void addScore()
{
myStatusCtrl.addScore();
}
public int getRoundNum()
{
return myStatusCtrl.getRoundNum();
}
public void subScore()
{
myStatusCtrl.subScore();
}
}
}
其中IUserAction接口定义了与玩家交互相关的动作,即空格发射飞碟和鼠标点击打飞碟;而IGameStatusOp接口则定义了与游戏状态相关的工作,即计回合数、加减玩家得分。
UserInterface
具体的用户接口UserInterface实现如下:
using CarolSum.com;
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class UserInterface : MonoBehaviour {
private IUserAction action;
// Use this for initialization
void Start () {
action = SceneController.getInstance() as IUserAction;
}
// Update is called once per frame
void Update () {
//检测用户按下空格 发射飞碟
if (Input.GetKeyDown(KeyCode.Space))
{
action.launchUFO();
}
//检测鼠标点击 击打飞碟
if (Input.GetMouseButtonDown(0))
{
Vector3 mousePosition = Input.mousePosition;
action.hitUFO(mousePosition);
}
}
}
编写完成UserInterface后将其挂载在Main Camera
上
UFOController
飞碟管理员类UFOController主要管理发射飞碟动作以及判断玩家是否击中飞碟。当场记SceneController发出一个发射飞碟指令时,UFOController首先判断当前是否有飞碟处于发射状态,若有则不发射新飞碟,否则根据当前回合所处的难度等级以一定的速度发射某颜色的若干飞碟。当玩家击中飞碟时UFOController通知ScenecController进行加分(addScore()
),并把被击中的飞碟回收(RecyclingUFO()
)。为使击中飞碟的效果更加明显与符合玩家认知,添加击中的爆炸效果createExplosion()
。代码如下:
using CarolSum.com;
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class UFOController : MonoBehaviour {
//预设文件
public GameObject PlaneItem, LauncherItem, ExplosionItem;
public Material greenMat, redMat, blueMat;
private GameObject plane, launcher, explosion;
private SceneController scene;
//发射每一个飞碟的时间间隔
private const float LAUNCH_GAP = 0.1f;
// Use this for initialization
void Start () {
scene = SceneController.getInstance();
scene.setUFOController(this);
plane = Instantiate(PlaneItem);
launcher = Instantiate(LauncherItem);
explosion = Instantiate(ExplosionItem);
}
// Update is called once per frame
void Update () {
UFOFactory.getInstance().detectLandingUFOs();
}
public void launchUFO()
{
int trailNum;
int temp = scene.getTrailNum();
if (temp>3){
trailNum = 3;
}
else{
trailNum = scene.getTrailNum();
}
//int trailNum = scene.getTrailNum() > 3 ? 3 : scene.getTrailNum();
Debug.Log("发射!");
if (!UFOFactory.getInstance().isLaunching())
{
StartCoroutine(launchUFOs(trailNum));
}
}
IEnumerator launchUFOs(int roundNum)
{
for(int i = 0; i<roundNum; i++)
{
GameObject UFO = UFOFactory.getInstance().getUFO();
UFO.transform.position = launcher.transform.position;
UFO.GetComponent<MeshRenderer>().material = getMaterial(scene.getTrailNum());
Vector3 force = getRandomForce();
UFO.GetComponent<Rigidbody>().AddForce(force, ForceMode.Impulse);
//每隔LAUNCH_GAP时间发射一个UFO
yield return new WaitForSeconds(LAUNCH_GAP);
}
}
private Vector3 getRandomForce()
{
int x = UnityEngine.Random.Range(-30, 31);
int y = UnityEngine.Random.Range(30, 41);
int z = UnityEngine.Random.Range(20, 31);
float t = 0.7f + scene.getTrailNum() / 20;
return new Vector3(x, y, z) * t;
}
public void hitUFO(Vector3 mousePos)
{
Ray ray = Camera.main.ScreenPointToRay(mousePos);
RaycastHit hit;
if(Physics.Raycast(ray, out hit))
{
if (hit.collider.gameObject.tag.Equals("UFO"))
{
createExplosion(hit.collider.gameObject.transform.position);
scene.addScore();
UFOFactory.getInstance().RecyclingUFO(hit.collider.gameObject);
}
}
}
private void createExplosion(Vector3 position)
{
explosion.transform.position = position;
explosion.GetComponent<ParticleSystem>().GetComponent<Renderer>().material = getMaterial(scene.getTrailNum());
explosion.GetComponent<ParticleSystem>().Play();
}
private Material getMaterial(int roundNum)
{
switch(roundNum % 3)
{
case 0:
return redMat;
case 1:
return greenMat;
case 2:
return blueMat;
default:
return redMat;
}
}
}
UFOFactory
飞碟工厂类UFOFactory中维护两个列表freeUFOList
和usedUFOList
,一个用来记录正在飞行的飞碟,一个用来记录空闲的飞碟。当需要发射飞碟时首先从空闲飞碟列表中找,当空闲列表中的空闲飞碟对象不足时才新建对象,当飞碟飞出边界或被用户击中时就被工厂回收,添加回空闲列表。代码如下:
using CarolSum.com;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class UFOFactory : System.Object {
private static UFOFactory instance; //单例工厂
private List<GameObject> usedUFOList = new List<GameObject>(); //使用中的飞碟
private List<GameObject> freeUFOList = new List<GameObject>(); //空闲的飞碟
private GameObject UFOItem; //飞碟预设
public static UFOFactory getInstance()
{
if (instance == null) instance = new UFOFactory();
return instance;
}
public GameObject getUFO()
{
if(freeUFOList.Count == 0)
{
GameObject newUFO = Camera.Instantiate(UFOItem);
usedUFOList.Add(newUFO);
return newUFO;
}
else
{
GameObject oldUFO = freeUFOList[0];
freeUFOList.RemoveAt(0);
oldUFO.SetActive(true);
usedUFOList.Add(oldUFO);
return oldUFO;
}
}
//update()检测飞镖落地,回收。此方法由GameModel的update()方法触发;
//飞碟未被击中,落地不扣分
public void detectLandingUFOs()
{
for(int i = 0; i < usedUFOList.Count; i++)
{
if(usedUFOList[i].transform.position.y <= -8)
{
usedUFOList[i].GetComponent<Rigidbody>().velocity = Vector3.zero;
usedUFOList[i].SetActive(false);
freeUFOList.Add(usedUFOList[i]);
usedUFOList.Remove(usedUFOList[i]);
i--;
//SceneController.getInstance().subScore();
}
}
}
//是否处于发射飞碟阶段,即空中是否有飞碟在飞
public bool isLaunching()
{
return (usedUFOList.Count > 0);
}
public void initItems(GameObject ufoItem)
{
UFOItem = ufoItem;
}
//飞镖被击中,回收
public void RecyclingUFO(GameObject UFOObject)
{
UFOObject.GetComponent<Rigidbody>().velocity = Vector3.zero;
UFOObject.SetActive(false);
freeUFOList.Add(UFOObject);
usedUFOList.Remove(UFOObject);
}
}
public class UFOFactoryBC: MonoBehaviour
{
public GameObject ufoItem;
private void Awake()
{
UFOFactory.getInstance().initItems(ufoItem);
}
}
StatusController
记分员StatusController类负责记录当前回合数以及玩家的得分情况,当记分员收到加分命令时就调用addSore()
为玩家加100分,当收到扣分命令时调用subScore()
为玩家减分(不能低于0),当玩家分数累积达到上限(300分)时就进入下一个trail,当trail大于10则进入下一回合,当玩家当前trail失败需要重新开始时则将分数清零。代码如下:
using CarolSum.com;
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class StatusController : MonoBehaviour
{
//各种预设资源
public GameObject canvasItem, roundTextItem, scoreTextItem, TipsTextItem;
//回合数
private int roundNum = 1;
//trial数
private int trialNum = 1;
//得分
private int score = 0;
//每一个trial的得分上界
private int scoreUpBound = 100;
//Tips显示的时间
private const float TIPS_TEXT_SHOW_TIME = 0.8f;
private GameObject canvas, roundText, scoreText, TipsText;
private SceneController scene;
// Use this for initialization
void Start()
{
scene = SceneController.getInstance();
scene.setStatusController(this);
canvas = Instantiate(canvasItem);
roundText = Instantiate(roundTextItem, canvas.transform);
roundText.GetComponent<Text>().text = "Round: " + roundNum + " Trial: " + trialNum;
scoreText = Instantiate(scoreTextItem, canvas.transform);
scoreText.GetComponent<Text>().text = "Score: " + score + " / " + (roundNum * 100);
TipsText = Instantiate(TipsTextItem, canvas.transform);
showTipsText();
}
// Update is called once per frame
void Update()
{
}
public int getRoundNum() { return roundNum; }
void addRoundNum()
{
roundNum++;
roundText.GetComponent<Text>().text = "Round: " + roundNum + " Trial: " + trialNum;
}
public int getScore()
{
return score;
}
public int getTrialNum() { return trialNum; }
public void addScore()
{
score += 100;
scoreText.GetComponent<Text>().text = "Score: " + score + " / " + scoreUpBound;
Debug.Log(scoreUpBound);
//当分数达到当前回合最大分数自动进入下一轮
if (score >= scoreUpBound)
{
trialNum++;
updateScoreUpBound();
roundText.GetComponent<Text>().text = "Round: " + roundNum + " Trial: " + trialNum;
resetScore();
showTipsText();
}
if (trialNum > 10)
{
addRoundNum();
trialNum = 1;
roundText.GetComponent<Text>().text = "Round: " + roundNum + " Trial: " + trialNum;
updateScoreUpBound();
resetScore();
showTipsText();
}
}
private void updateScoreUpBound()
{
scoreUpBound = trialNum > 3 ? 300 : trialNum * 100;
}
private void showTipsText()
{
TipsText.GetComponent<Text>().text = "Round " + roundNum + ": Trial " + trialNum + "!";
TipsText.SetActive(true);
StartCoroutine(waitForSomeTimeAndDisappearTipsText());
}
IEnumerator waitForSomeTimeAndDisappearTipsText()
{
yield return new WaitForSeconds(TIPS_TEXT_SHOW_TIME);
TipsText.SetActive(false);
}
public void resetScore()
{
score = 0;
scoreText.GetComponent<Text>().text = "Score: " + score + " / " + scoreUpBound;
}
public void subScore()
{
score = score >= 100 ? score - 100 : 0;
scoreText.GetComponent<Text>().text = "Score: " + score + " / " + scoreUpBound;
}
}
最后创建一个Empty对象,然后将编写完成的脚本StatusController
、UFOController
以及UFOFactory
挂载在该空对象上。
游戏运行过程如下: