目录
编写一个简单的鼠标打飞碟(Hit UFO)游戏
游戏内容要求:
游戏有 n 个 round,每个 round 都包括10 次 trial;
每个 trial 的飞碟的色彩、大小、发射位置、速度、角度、同时出现的个数都可能不同。它们由该 round 的 ruler 控制;
每个 trial 的飞碟有随机性,总体难度随 round 上升;
鼠标点中得分,得分规则按色彩、大小、速度不同计算,规则可自由设定。
游戏的要求:
使用带缓存的工厂模式管理不同飞碟的生产与回收,该工厂必须是场景单实例的!具体实现见参考资源 Singleton 模板类
尽可能使用前面 MVC 结构实现人机交互与游戏模型分离
程序设计
MVC结构
模型(Model)
场景中的所有GameObject
UserGUI(View)
显示模型,将人机交互事件交给控制器处理
- 处收 Input 事件
private void Update()
{
if (Input.GetButtonDown("Fire1"))
{
//Debug.Log("Fire1");
Vector3 pos = Input.mousePosition;
action.hit(pos);
}
}
- 渲染 GUI ,接收事件
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();
}
}
Controller
与上两个作业一样。
- 使用单例模式,由
Director
类统筹整个游戏 - 由场记
XXXSceneController
来管理本次场景所有的游戏对象,响应外部输入事件 UserAction
接口定义了用户行为
唯一的改动是玩家与游戏交互的接口,包含玩家的两个动作:
public interface UserAction
{
void hit(Vector3 pos);
void restart();
}
FirstController
加载资源并控制整个游戏的流程。
加载资源
public void LoadResources()
{
actionManager = gameObject.AddComponent<CCActionManager>() as CCActionManager;
this.gameObject.AddComponent<DiskFactory>();
ruler = new Ruler();
scoreRecorder = new ScoreRecorder();
}
在Update函数中,设定每隔一段时间就抛出一个disk,每个round总共抛出10个Disk;同时判断是否到达晋级下一轮的条件或是否失败。
void Update()
{
if (round != -1&&ruler.enterNextRound(round, scoreRecorder.score))
{
round++;
trial = 0;
getDisksForNextRound();
userGUI.score = this.score = 0;
scoreRecorder.reset();
userGUI.targetThisRound = ruler.getTargetThisRound(round);
}
else if (round != -1&&!ruler.enterNextRound(round, scoreRecorder.score) && 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;
}
每一轮的开始,FirstController会从diskFactory中一次性拿够10个Disk放入queue中。
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);
}
}
抛出时,从queue中拿出一个,通过ruler为其设定好属性,然后将其交给动作管理器抛出。
public void throwDisk()
{
if (disksQueue.Count != 0)
{
GameObject disk = disksQueue.Dequeue();
ruler.setDiskProperty(disk, round);
disk.SetActive(true);
actionManager.diskFly(disk, disk.GetComponent<DiskData>().angle, disk.GetComponent<DiskData>().power);
}
}
动作管理器
动作管理器可以在上一次作业的基础上进行修改。
ISSActionCallback
为动作接口SSAction
为动作父类,规定所有Action
的属性和方法DiskFlyAction
继承自SSAction
,规定了一个Disk
的动作,动作即飞碟的移动,由两个参数决定:受力角度和受力大小。构造函数初始化初始速度,Update函数模拟物体的坐标移动过程,当disk在画面之外(通过坐标判定),就callback通知动作做完。
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);
}
}
SSActionManager
函数。动作管理器,管理Action List中的每个Action(不用关心是单一Action还是连续Action)。CCActionManager
。SSActionManager的子类,封装了操作Action的相关类的函数,使得FirstController调用起来更简洁。
public void diskFly(GameObject disk, float angle, float power)
{
fly = DiskFlyAction.GetSSAction(angle, power); //disk.GetComponent<Disk>().direction, angle, power);
this.RunAction(disk, fly, this);
}
DiskData
简单地记录飞碟的属性,作为一个组件添加到具体的GameObject上
public class DiskData : MonoBehaviour
{
public float size;
public Color color;
//move
public float angle;
public float power;
}
DiskFactory
- 一个单实例类,用前面场景单实例创建
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;
}
}
}
然后使用代码
Singleton<DiskFactory>.Instance
获得该对象
- 负责飞碟的生产和回收。
public GameObject GetDisk(int round)
{
GameObject newDisk = null;
if (free.Count > 0)
{
newDisk = free[0].gameObject;
free.Remove(free[0]);
}
else
{
newDisk = GameObject.Instantiate<GameObject>(diskPrefab, Vector3.zero, Quaternion.identity);
newDisk.name = nameIndex.ToString();
nameIndex++;
}
used.Add(newDisk);
return newDisk;
}
public void FreeDisk(GameObject usedDisk)
{
if (usedDisk != null)
{
usedDisk.SetActive(false);
used.Remove(usedDisk);
free.Add(usedDisk);
}
}
- 管理两个列表,记录已使用和空闲的飞碟数据。
private List<GameObject> used = new List<GameObject>(); //正在使用
private List<GameObject> free = new List<GameObject>(); //使用过已被释放的,可以重复使用
- 使用模板模式根据预制和规则制作飞碟(diskPrefab)
private void Awake()
{
diskPrefab = GameObject.Instantiate<GameObject>(Resources.Load<GameObject>("Prefabs/disk"), Vector3.zero, Quaternion.identity);
diskPrefab.name = "prefab";
diskPrefab.AddComponent<DiskData>();
diskPrefab.SetActive(false);
nameIndex = 0;
}
newDisk = GameObject.Instantiate<GameObject>(diskPrefab, Vector3.zero, Quaternion.identity);
Ruler
Ruler根据Round设置每个 trial 的飞碟的大小、速度、出现的时间间隔。
色彩、发射位置、角度则与Round无关。
public class Ruler
{
public void setDiskProperty(GameObject disk, int round)
{
disk.transform.position = this.setRandomInitPos();
disk.GetComponent<Renderer>().material.color = setRandomColor();
disk.transform.localScale = setScale(round);
disk.GetComponent<DiskData>().angle = setRandomAngle();
disk.GetComponent<DiskData>().power = setPower(round);
}
public Vector3 setRandomInitPos()
{
float x = Random.Range(-10f, 10f);
float y = Random.Range(-1f, 5f);
float z = Random.Range(-3f, 3f);
return new Vector3(x, y, z);
}
public Vector4 setRandomColor()
{
int r = Random.Range(0f, 1f) > 0.5 ? 255 : 0;
int g = Random.Range(0f, 1f) > 0.5 ? 255 : 0;
int b = Random.Range(0f, 1f) > 0.5 ? 255 : 0;
return new Vector4(r, g, b, 1);
}
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 setRandomAngle()
{
return Random.Range(-360f, 360f);
}
public float setPower(int round)
{
return round;
}
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,int score)
{
if (round != -1 && score >= (5 + round > 10 ? 10 : 5 + round))
{
return true;
}
return false;
}
}
另外,Ruler还负责判断游戏是否可以进入下一轮/失败,round越大,要求进入下一轮的分数也越大。一轮10个trial,最高10分。
public bool enterNextRound(int round)
{
if (round != -1 && this.score[round - 1] >= (5 + round > 10 ? 10 : 5 + round))
{
return true;
}
return false;
}
ScoreRecorder
记分员保存分数值,按飞碟的数据计分,重置将分数值清零
public class ScoreRecorder
{
public int score;
public void record(GameObject disk)
{
int s = 1;
if (disk.GetComponent<Renderer>().material.color == new Color(255, 0, 0, 1)) s += 1;
score+=s;
}
public void reset()
{
score = 0;
}
}