编写一个简单的鼠标打飞碟(Hit UFO)游戏 游戏内容要求: 游戏有 n 个 round,每个 round 都包括10 次 trial; 每个 trial 的飞碟的色彩、大小、发射位置、速度、角度、同时出现的个数都可能不同。它们由该 round 的 ruler 控制; 每个 trial 的飞碟有随机性,总体难度随 round 上升; 鼠标点中得分,得分规则按色彩、大小、速度不同计算,规则可自由设定。 游戏的要求: 使用带缓存的工厂模式管理不同飞碟的生产与回收,该工厂必须是场景单实例的!具体实现见参考资源 Singleton 模板类 尽可能使用前面 MVC 结构实现人机交互与游戏模型分离
场景中的所有GameObject
显示模型,将人机交互事件交给控制器处理
处收 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(); } }与上两个作业一样。
使用单例模式,由Director类统筹整个游戏由场记XXXSceneController来管理本次场景所有的游戏对象,响应外部输入事件UserAction接口定义了用户行为 唯一的改动是玩家与游戏交互的接口,包含玩家的两个动作: public interface UserAction { void hit(Vector3 pos); void restart(); }加载资源并控制整个游戏的流程。 加载资源
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); }简单地记录飞碟的属性,作为一个组件添加到具体的GameObject上
public class DiskData : MonoBehaviour { public float size; public Color color; //move public float angle; public float power; }然后使用代码 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根据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; }记分员保存分数值,按飞碟的数据计分,重置将分数值清零
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; } }代码传送门