游戏规则:
游戏分多个回合,每个回合有N个飞碟,玩家按空格后,321倒数3秒,飞碟飞出,点击鼠标,子弹飞出。飞碟落地或被击中,则准备下一次射击。每回合飞碟的大小、颜色、发射位置、发射角度、每次发射的数量可以变化。
游戏效果:
录了好几次,每次飞碟都打不中啊!_(:з」∠)_
游戏类图:
代码说明:
一、飞碟回收工厂类(DiskFactory)
创建新的命名空间Com.Mygame,单例类DiskFactory和SceneController都定义其中。飞碟工厂类的目的是管理飞碟实例,同时对外屏蔽飞碟实例的的提取和回收细节,对于需要使用飞碟的其他对象,只能使用工厂类提供的3个函数,分别是getDisk()、getDiskObject()、free()。
using UnityEngine; using System.Collections; using System.Collections.Generic; using Com.Mygame; namespace Com.Mygame { public class DiskFactory : System.Object { private static DiskFactory _instance; private static List<GameObject> diskList; // 飞碟队列 public GameObject diskTemplate; // 预设对象 public static DiskFactory getInstance() { if (_instance == null) { _instance = new DiskFactory(); diskList = new List<GameObject>(); } return _instance; } // 获取可用飞碟id public int getDisk() { for (int i = 0; i < diskList.Count; ++i) { if (!diskList[i].activeInHierarchy) { return i; // 飞碟空闲 } } // 无空闲飞碟,则实例新的飞碟预设 diskList.Add(GameObject.Instantiate(diskTemplate) as GameObject); return diskList.Count-1; } // 获取飞碟对象 public GameObject getDiskObject(int id) { if (id > -1 && id < diskList.Count) { return diskList[id]; } return null; } // 回收飞碟 public void free(int id) { if (id > -1 && id < diskList.Count) { // 重置飞碟速度 diskList[id].rigidbody.velocity = Vector3.zero; // 重置飞碟大小 diskList[id].transform.localScale = diskTemplate.transform.localScale; diskList[id].SetActive(false); } } } } public class DiskFactoryBC : MonoBehaviour { public GameObject disk; void Awake () { // 初始化预设对象 DiskFactory.getInstance().diskTemplate = disk; } }
有几点需要注意:
1) 当且仅当请求队列里的所有对象都在被使用(飞碟在场景中活跃)时,才会发生实例化,此时队列会变长。
2) getDisk返回的是可用飞碟在队列里的index,这是为了方便free。
3) free通过index找到飞碟在队列中的位置,并将飞碟设置为不活跃的。注意,由于飞碟使用了刚体组件,回收时需要把速度重置,并且大小可能会被改变,也应该重置。
二、用户界面类(UserInterface)
用户界面有两大功能,一是处理用户键入,二是显示得分和倒计时等。用户键入有两种:鼠标左键和空格。左键发射子弹,空格发射飞碟。显示有三种:得分、回合和倒计时。
子弹射击的思路:当用户点击鼠标时,从摄像机到鼠标创建一条射线,射线的方向即是子弹发射的方向,子弹采用刚体组件,因此发射子弹只需要给子弹施加一个力。子弹对象只有一个,下一次发射子弹时,必须改变子弹的位置(虽然有了刚体组件不建议修改transform,但也没有其它方法改变子弹位置了吧)。为了不让子弹继承上一次发射的速度,必须将子弹的速度归零重置。
子弹的击中判断:采用射线而不是物理引擎,因为物理引擎在高速物体碰撞时经常不能百分百检测得到。
发射飞碟的思路:调用用户接口。
显示的思路:得分和回合直接通过查询接口获得。倒计时显示前通过查询接口判断是否正在倒计时,如果是,那么再通过查询接口获得倒计时时间。如果回合发生改变,则显示新的回合,直到用户按下空格。
using UnityEngine; using UnityEngine.UI; using System.Collections; using Com.Mygame; public class UserInterface : MonoBehaviour { public Text mainText; // 显示主提示:倒计时、新回合 public Text scoreText; // 显示得分 public Text roundText; // 显示回合 private int round; // 当前回合 public GameObject bullet; // 子弹 public ParticleSystem explosion; // 爆炸粒子 public float fireRate = .25f; // 开枪间隔 public float speed = 500f; // 子弹速度 private float nextFireTime; // 下一次开枪时间 private IUserInterface userInt; // 用户接口 private IQueryStatus queryInt; // 查询接口 void Start() { bullet = GameObject.Instantiate(bullet) as GameObject; explosion = GameObject.Instantiate(explosion) as ParticleSystem; userInt = SceneController.getInstance() as IUserInterface; queryInt = SceneController.getInstance() as IQueryStatus; } void Update () { if (queryInt.isCounting()) { // 显示倒计时 mainText.text = ((int)queryInt.getEmitTime()).ToString(); } else { if (Input.GetKeyDown("space")) { userInt.emitDisk(); // 发射飞碟 } if (queryInt.isShooting()) { mainText.text = ""; // 射击开始,隐藏主提示 } // 发射子弹 if (queryInt.isShooting() && Input.GetMouseButtonDown(0) && Time.time > nextFireTime ) { nextFireTime = Time.time + fireRate; Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition); // 摄像机到鼠标射线 bullet.rigidbody.velocity = Vector3.zero; // 子弹刚体速度重置 bullet.transform.position= transform.position; // 子弹从摄像机位置射出 bullet.rigidbody.AddForce(ray.direction*speed, ForceMode.Impulse); RaycastHit hit; if (Physics.Raycast(ray, out hit) && hit.collider.gameObject.tag == "Disk") { // 播放爆炸粒子特效 explosion.transform.position = hit.collider.gameObject.transform.position; explosion.renderer.material.color = hit.collider.gameObject.renderer.material.color; explosion.Play(); // 击中飞碟设置为不活跃,自动回收 hit.collider.gameObject.SetActive(false); } } } roundText.text = "Round: " + queryInt.getRound().ToString(); scoreText.text = "Score: " + queryInt.getPoint().ToString(); // 如果回合更新,主提示显示新回合 if (round != queryInt.getRound()) { round = queryInt.getRound(); mainText.text = "Round " + round.ToString() + " !"; } } }
三、场景控制器类(SceneController)
场景控制类主要实现接口定义和保存注入对象。另外它有两个私有变量round和point,分别记录游戏正在进行的回合,以及玩家目前的得分。
using UnityEngine; using System.Collections; using Com.Mygame; namespace Com.Mygame { public interface IUserInterface { void emitDisk(); } public interface IQueryStatus { bool isCounting(); bool isShooting(); int getRound(); int getPoint(); int getEmitTime(); } public interface IJudgeEvent { void nextRound(); void setPoint(int point); } public class SceneController : System.Object, IQueryStatus, IUserInterface, IJudgeEvent { private static SceneController _instance; private SceneControllerBC _baseCode; private GameModel _gameModel; private Judge _judge; private int _round; private int _point; public static SceneController getInstance() { if (_instance == null) { _instance = new SceneController(); } return _instance; } public void setGameModel(GameModel obj) { _gameModel = obj; } internal GameModel getGameModel() { return _gameModel; } public void setJudge(Judge obj) { _judge = obj; } internal Judge getJudge() { return _judge; } public void setSceneControllerBC(SceneControllerBC obj) { _baseCode = obj; } internal SceneControllerBC getSceneControllerBC() { return _baseCode; } // 操作接口 public void emitDisk() { _gameModel.prepareToEmitDisk(); } // 查询接口 public bool isCounting() { return _gameModel.isCounting(); } public bool isShooting() { return _gameModel.isShooting(); } public int getRound() { return _round; } public int getPoint() { return _point; } public int getEmitTime() { return (int)_gameModel.timeToEmit + 1; } // 得分接口 public void setPoint(int point) { _point = point; } public void nextRound() { _point = 0; _baseCode.loadRoundData(++_round); } } }
四、关卡类(SceneControllerBC)
关卡类保存了各个round飞碟的大小、颜色、发射位置、发射角度、发射数量等信息。它只有一个函数loadRoundData(),用来初始化游戏场景。
public class SceneControllerBC : MonoBehaviour { private Color color; private Vector3 emitPos; private Vector3 emitDir; private float speed; void Awake() { SceneController.getInstance().setSceneControllerBC(this); } public void loadRoundData(int round) { switch(round) { case 1: // 第一关 color = Color.green; emitPos = new Vector3(-2.5f, 0.2f, -5f); emitDir = new Vector3(24.5f, 40.0f, 67f); speed = 4; SceneController.getInstance().getGameModel().setting(1, color, emitPos, emitDir.normalized, speed, 1); break; case 2: // 第二关 color = Color.red; emitPos = new Vector3(2.5f, 0.2f, -5f); emitDir = new Vector3(-24.5f, 35.0f, 67f); speed = 4; SceneController.getInstance().getGameModel().setting(1, color, emitPos, emitDir.normalized, speed, 2); break; } } }
五、游戏规则类(Judge)
游戏规则单独作为一个类,有利于日后修改。这里需要处理的规则无非就两个,得分和失分。另外,得分需要判断是否能晋级下一关。能就调用接口函数nextRound()。
using UnityEngine; using System.Collections; using Com.Mygame; public class Judge : MonoBehaviour { public int oneDiskScore = 10; public int oneDiskFail = 10; public int disksToWin = 4; private SceneController scene; void Awake() { scene = SceneController.getInstance(); scene.setJudge(this); } void Start() { scene.nextRound(); // 默认开始第一关 } // 击中飞碟得分 public void scoreADisk() { scene.setPoint(scene.getPoint() + oneDiskScore); if (scene.getPoint() == disksToWin*oneDiskScore) { scene.nextRound(); } } // 掉落飞碟失分 public void failADisk() { scene.setPoint(scene.getPoint() - oneDiskFail); } }
六、游戏场景类(GameModel)
场景类是整个飞碟射击游戏的核心类,主要负责飞碟动作的处理。我是这样设计的:首先需要倒计时功能,可以通过几个整型变量和布尔变量完成。另外需要飞碟发射功能,通过setting函数保存好飞碟的发射信息,每次倒计时完成后,通过emitDisks获取飞碟对象,并通过发射信息初始化飞碟,再给飞碟一个力就可以发射了。而飞碟的回收在Update里完成,一种是飞碟被击中(飞碟不在场景中)了,需要调用Judge获得分数。另一种是飞碟在场景中,但是掉在地上了,需要调用Judge丢失分数。
using UnityEngine; using System.Collections; using System.Collections.Generic; using Com.Mygame; public class GameModel : MonoBehaviour { public float countDown = 3f; // 飞碟发射倒计时总时间 public float timeToEmit; // 飞碟发射倒计时剩余时间 private bool counting; // 正在倒计时 private bool shooting; // 正在射击 public bool isCounting() { return counting; } public bool isShooting() { return shooting; } private List<GameObject> disks = new List<GameObject>(); // 发射的飞碟对象列表 private List<int> diskIds = new List<int>(); // 发射的飞碟id列表 private int diskScale; // 飞碟大小 private Color diskColor; // 飞碟颜色 private Vector3 emitPosition; // 发射位置 private Vector3 emitDirection; // 发射方向 private float emitSpeed; // 发射速度 private int emitNumber; // 发射数量 private bool emitEnable; // 允许新的发射事件 private SceneController scene; void Awake () { scene = SceneController.getInstance(); scene.setGameModel(this); } // 初始化场景 public void setting(int scale, Color color, Vector3 emitPos, Vector3 emitDir, float speed, int num) { diskScale = scale; diskColor = color; emitPosition = emitPos; emitDirection = emitDir; emitSpeed = speed; emitNumber = num; } // 准备下一次发射 public void prepareToEmitDisk() { if (!counting && !shooting) { timeToEmit = countDown; emitEnable = true; } } // 发射飞碟 void emitDisks() { for (int i = 0; i < emitNumber; ++i) { diskIds.Add(DiskFactory.getInstance().getDisk()); disks.Add(DiskFactory.getInstance().getDiskObject(diskIds[i])); disks[i].transform.localScale *= diskScale; disks[i].renderer.material.color = diskColor; disks[i].transform.position = new Vector3(emitPosition.x, emitPosition.y+i, emitPosition.z); disks[i].SetActive(true); disks[i].rigidbody.AddForce(emitDirection*Random.Range(emitSpeed*5, emitSpeed*10)/10, ForceMode.Impulse); } } // 回收飞碟 void freeADisk(int i) { DiskFactory.getInstance().free(diskIds[i]); disks.RemoveAt(i); diskIds.RemoveAt(i); } void FixedUpdate() { if (timeToEmit > 0) { counting = true; timeToEmit -= Time.deltaTime; } else { counting = false; if (emitEnable) { emitDisks(); // 发射飞碟 emitEnable = false; shooting = true; } } } void Update () { for (int i = 0; i < disks.Count; ++i) { if (!disks[i].activeInHierarchy) { // 飞碟不在场景中 scene.getJudge().scoreADisk(); // 得分 freeADisk(i); } else if (disks[i].transform.position.y < 0) { // 飞碟在场景中但落地 scene.getJudge().failADisk(); // 失分 freeADisk(i); } } if (disks.Count == 0) { shooting = false; } } }
其他说明:
场景中预置的所有对象:
一、摄像机
可将所有脚本挂载在摄像机上。摄像机的参数:
二、预设
子弹(Bullet):
飞碟(Disk):
粒子(Particle System):
地板材质:
三、文字
在场景编辑器里右键创建UI元素Text,调整 text 对齐位置和文字大小即可。
四、脚本设置
最后,将5个脚本挂载在组摄像机上,将预设拖入相应的脚本中:
OK!游戏完成了!
我的制作顺序是这样子的,游戏开始的时候先不忙着写脚本,把预设弄好,包括摄像机角度,粒子效果等。写脚本先写子弹射击,然后写飞碟工厂,再写游戏场景,这时候就已经有了一个小Demo可以玩了。然后添加场景控制类,完善用户界面,使得可以显示回合和分数等信息。最后写关卡类和规则类。