unity3d-射击飞碟改进版
本文章参考Unity3D学习笔记(6)—— 飞碟射击游戏 胡江川同学的博客,根据他的思路重新进行实践学习,并且加以理解修改整理为如下博文。有需要可以参考原文,写的详细又清楚。
题目要求
编制一个射飞碟游戏。
具体要求如下:
- 假设有一支枪在摄像机位置(0,1,-10),在(0,0,0-10-20)放置三个小球作为距离标记,调整视角直到小球在下中部
- 将鼠标所在平面坐标,转换为子弹(球体)射出的角度方向。子弹使用物理引擎,初速度恒定。
- 游戏要分多个 round , 飞碟数量每个 round 都是 n 个,但色彩,大小;发射位置,速度,角度,每次发射数量按预定规则变化。
- 用户按空格后,321倒数3秒,飞碟飞出(物理引擎控制),点击鼠标,子弹飞出。飞碟落地,或被击中,则准备下一次射击。
- 以下是一些技术要求:
- 子弹仅需要一个,不用时处于 deactive 状态
- 飞碟用一个带缓存的工厂生产,template 中放置预制的飞碟对象
- 程序类图设计大致如下:
步骤
设置摄像机和预设
通过屏幕的调整来设置摄像机的参数:
摄像机:
子弹:
飞碟:
粒子效果:
脚本实现子弹射击
脚本挂在在摄像机上
子弹射击的思路:当用户点击鼠标时,从摄像机到鼠标创建一条射线,射线的方向即是子弹发射的方向,子弹采用刚体组件,因此发射子弹只需要给子弹施加一个力。子弹对象只有一个,下一次发射子弹时,必须改变子弹的位置(虽然有了刚体组件不建议修改transform,但也没有其它方法改变子弹位置了吧)。为了不让子弹继承上一次发射的速度,必须将子弹的速度归零重置。
子弹的击中判断:采用射线而不是物理引擎,因为物理引擎在高速物体碰撞时经常不能百分百检测得到。
1 | using UnityEngine; |
2 | using UnityEngine.UI; |
3 | using System.Collections; |
4 | |
5 | |
6 | public class UserInterface : MonoBehaviour { |
7 | |
8 | public GameObject bullet; |
9 | public float fireRate = .25f; |
10 | public float speed = 500f; |
11 | |
12 | private float nextFireTime; |
13 | |
14 | |
15 | |
16 | // Use this for initialization |
17 | void Start () { |
18 | bullet = GameObject.Instantiate (bullet) as GameObject; |
19 | |
20 | } |
21 | |
22 | // Update is called once per frame |
23 | void Update () { |
24 | if (Input.GetMouseButtonDown (0)) { |
25 | nextFireTime = Time.time + fireRate; |
26 | |
27 | Ray ray = Camera.main.ScreenPointToRay (Input.mousePosition); |
28 | bullet.GetComponent<Rigidbody>().velocity = Vector3.zero; // 速度设为0 |
29 | bullet.transform.position = transform.position; // 脚本挂载在摄像机上,子弹从摄像机位置飞出 |
30 | bullet.GetComponent<Rigidbody>().AddForce(ray.direction*speed, ForceMode.Impulse); // 给子弹添加力 |
31 | |
32 | } |
33 | } |
34 | |
35 | } |
运行之后,发现子弹可以从摄像机的方向射出了。
完成飞碟工厂
创建新的命名空间Com.Mygame,单例类DiskFactory和SceneController都定义其中。飞碟工厂类的目的是管理飞碟实例,同时对外屏蔽飞碟实例的的提取和回收细节,对于需要使用飞碟的其他对象,只能使用工厂类提供的3个函数,分别是getDisk()、getDiskObject()、free()。
有几点需要注意:
- 当且仅当请求队列里的所有对象都在被使用(飞碟在场景中活跃)时,才会发生实例化,此时队列会变长。
- getDisk返回的是可用飞碟在队列里的index,这是为了方便free。
- free通过index找到飞碟在队列中的位置,并将飞碟设置为不活跃的。注意,由于飞碟使用了刚体组件,回收时需要把速度重置,并且大小可能会被改变,也应该重置。
1 | using UnityEngine; |
2 | using System.Collections; |
3 | using System.Collections.Generic; |
4 | using Com.Mygame; |
5 | |
6 | namespace Com.Mygame { |
7 | public class DiskFactory: System.Object { |
8 | private static DiskFactory _instance; |
9 | private static List<GameObject> diskList; |
10 | public GameObject diskPrefab; |
11 | |
12 | public static DiskFactory getInstance() { |
13 | if (_instance == null) { |
14 | _instance = new DiskFactory(); |
15 | diskList = new List<GameObject>(); |
16 | } |
17 | return _instance; |
18 | } |
19 | |
20 | // 获取可用飞碟id |
21 | public int getDisk() { |
22 | for (int i = 0; i < diskList.Count; ++i) { |
23 | if (!diskList[i].activeInHierarchy) { |
24 | return i; // 飞碟空闲 |
25 | } |
26 | } |
27 | // 无空闲飞碟,则实例新的飞碟预设 |
28 | diskList.Add(GameObject.Instantiate(diskPrefab) as GameObject); |
29 | return diskList.Count-1; |
30 | } |
31 | // 获取飞碟对象 |
32 | public GameObject getDiskObject(int id) { |
33 | if (id >= 0 && id < diskList.Count) { |
34 | return diskList[id]; |
35 | } |
36 | return null; |
37 | } |
38 | // 回收飞碟 |
39 | public void free(int id) { |
40 | if (id >= 0 && id < diskList.Count) { |
41 | // 重置飞碟速度 |
42 | diskList[id].GetComponent<Rigidbody>().velocity = Vector3.zero; |
43 | // 重置飞碟大小 |
44 | diskList[id].transform.localScale = diskPrefab.transform.localScale; |
45 | diskList[id].SetActive(false); |
46 | } |
47 | } |
48 | } |
49 | |
50 | } |
51 | |
52 | public class DiskFactoryBasecode : MonoBehaviour { |
53 | |
54 | public GameObject disk; |
55 | |
56 | void Awake () { |
57 | // 初始化预设对象 |
58 | DiskFactory.getInstance().diskPrefab = disk; |
59 | } |
60 | } |
完成游戏场景
场景类是整个飞碟射击游戏的核心类,主要负责飞碟动作的处理。胡同学是这样设计的:首先需要倒计时功能,可以通过几个整型变量和布尔变量完成。另外需要飞碟发射功能,通过setting函数保存好飞碟的发射信息,每次倒计时完成后,通过emitDisks获取飞碟对象,并通过发射信息初始化飞碟,再给飞碟一个力就可以发射了。而飞碟的回收在Update里完成,一种是飞碟被击中(飞碟不在场景中)了,需要调用Judge获得分数。另一种是飞碟在场景中,但是掉在地上了,需要调用Judge丢失分数。
1 | using UnityEngine; |
2 | using System.Collections; |
3 | using System.Collections.Generic; |
4 | using Com.Mygame; |
5 | |
6 | public class GameModel : MonoBehaviour { |
7 | |
8 | public float countDown = 3f; |
9 | public float timeToEmit; |
10 | private bool counting; |
11 | private bool shooting; |
12 | public bool isCounting() { |
13 | return counting; |
14 | } |
15 | public bool isShooting() { |
16 | return shooting; |
17 | } |
18 | |
19 | private List<GameObject> disks = new List<GameObject> (); |
20 | private List<int> diskIds = new List<int> (); |
21 | private int diskScale; |
22 | private Color diskColor; |
23 | |
24 | private Vector3 emitPosition; |
25 | private Vector3 emitDirection; |
26 | |
27 | private float emitSpeed; |
28 | private int emitNumber; |
29 | private bool emitEnable; |
30 | |
31 | void Awake() { |
32 | |
33 | } |
34 | |
35 | public void setting(int scale, Color color, Vector3 emitPos, Vector3 emitDir, float speed, int num) { |
36 | diskScale = scale; |
37 | diskColor = color; |
38 | emitPosition = emitPos; |
39 | emitDirection = emitDir; |
40 | emitSpeed = speed; |
41 | emitNumber = num; |
42 | } |
43 | |
44 | public void prepareToEmitDisk() { |
45 | if (!counting && !shooting) { |
46 | timeToEmit = countDown; |
47 | emitEnable = true; |
48 | } |
49 | } |
50 | |
51 | void emitDisks() { |
52 | for (int i = 0; i < emitNumber; i++) { |
53 | diskIds.Add (DiskFactory.getInstance ().getDisk ()); |
54 | disks.Add (DiskFactory.getInstance ().getDiskObject (diskIds [i])); |
55 | disks [i].transform.localScale *= diskScale; |
56 | disks [i].GetComponent<Renderer> ().material.color = diskColor; |
57 | disks [i].transform.position = new Vector3 (emitPosition.x, emitPosition.y + i, emitPosition.z); |
58 | disks [i].SetActive (true); |
59 | disks [i].GetComponent<Rigidbody> ().AddForce (emitDirection * Random.Range (emitSpeed * 5, emitSpeed * 10) / 10, ForceMode.Impulse); |
60 | } |
61 | } |
62 | |
63 | void freeDisk(int i) { |
64 | DiskFactory.getInstance ().free (diskIds [i]); |
65 | disks.RemoveAt (i); |
66 | diskIds.RemoveAt (i); |
67 | } |
68 | |
69 | void FixedUpdate() { |
70 | if (timeToEmit > 0) { |
71 | counting = true; |
72 | timeToEmit -= Time.deltaTime; |
73 | } else { |
74 | counting = false; |
75 | if (emitEnable) { |
76 | emitDisks (); |
77 | emitEnable = false; |
78 | shooting = true; |
79 | } |
80 | } |
81 | } |
82 | |
83 | |
84 | // Use this for initialization |
85 | void Start () { |
86 | |
87 | } |
88 | |
89 | // Update is called once per frame |
90 | void Update () { |
91 | for (int i = 0; i < disks.Count; i++) { |
92 | if (!disks [i].activeInHierarchy) { |
93 | freeDisk (i); |
94 | } else if (disks [i].transform.position.y < 0) { |
95 | freeDisk (i); |
96 | } |
97 | } |
98 | |
99 | if (disks.Count == 0) { |
100 | shooting = false; |
101 | } |
102 | } |
103 | } |
场景控制器
场景控制类主要实现接口定义和保存注入对象。另外它有两个私有变量round和point,分别记录游戏正在进行的回合,以及玩家目前的得分。
1 | using UnityEngine; |
2 | using System.Collections; |
3 | using Com.Mygame; |
4 | |
5 | namespace Com.Mygame { |
6 | public interface IUserInterface { |
7 | void emitDisk(); |
8 | } |
9 | |
10 | public interface IQueryStatus { |
11 | bool isCounting(); |
12 | bool isShooting(); |
13 | int getRound(); |
14 | int getPoint(); |
15 | int getEmitTime(); |
16 | } |
17 | |
18 | public class SceneController : System.Object, IQueryStatus, IUserInterface { |
19 | |
20 | private static SceneController _instance; |
21 | private GameModel _gameModel; |
22 | private SceneControllerBaseCode _baseCode; |
23 | private int _round; |
24 | private int _point; |
25 | |
26 | public static SceneController getInstance() { |
27 | if (_instance == null) { |
28 | _instance = new SceneController (); |
29 | } |
30 | return _instance; |
31 | } |
32 | |
33 | public void setSceneControllerBaseCode (SceneControllerBaseCode obj) { |
34 | _baseCode = obj; |
35 | } |
36 | internal SceneControllerBaseCode getSceneControllerBC() { |
37 | return _baseCode; |
38 | } |
39 | |
40 | public void setGameModel(GameModel obj) { |
41 | _gameModel = obj; |
42 | } |
43 | |
44 | // 当前程序或派生类可用 |
45 | internal GameModel getGameModel() { |
46 | return _gameModel; |
47 | } |
48 | |
49 | public void emitDisk() { |
50 | _gameModel.prepareToEmitDisk (); |
51 | } |
52 | |
53 | public bool isCounting() { |
54 | return _gameModel.isCounting (); |
55 | } |
56 | public bool isShooting() { |
57 | return _gameModel.isShooting (); |
58 | } |
59 | public int getRound() { |
60 | return _round; |
61 | } |
62 | public int getPoint() { |
63 | return _point; |
64 | } |
65 | public int getEmitTime() { |
66 | return (int)_gameModel.timeToEmit + 1; |
67 | } |
68 | |
69 | public void setPoint(int point) { |
70 | _point = point; |
71 | } |
72 | public void nextRound() { |
73 | _point = 0; |
74 | } |
75 | |
76 | } |
77 | } |
78 | public class SceneControllerBaseCode : MonoBehaviour { |
79 | private Color color; |
80 | private Vector3 emitPos; |
81 | private Vector3 emitDir; |
82 | private float speed; |
83 | |
84 | void Awake() { |
85 | SceneController.getInstance().setSceneControllerBaseCode(this); |
86 | } |
87 | void Start() { |
88 | color = Color.green; |
89 | emitPos = new Vector3(-2.5f, 0.2f, -5f); |
90 | emitDir = new Vector3(24.5f, 40.0f, 67f); |
91 | speed = 4; |
92 | SceneController.getInstance().getGameModel().setting(1, color, emitPos, emitDir.normalized, speed, 1); |
93 | } |
94 | } |
完成了目前的场景控制器之后,在GameModel中添加场景控制器
1 | private SceneController scene; |
2 | |
3 | void Awake() { |
4 | scene = SceneController.getInstance (); |
5 | scene.setGameModel (this); |
6 | } |
完善UserInterface
第一步, 在场景编辑器里右键创建UI元素Text,调整 text 对齐位置和文字大小即可。
第二步,对UserInterface的完善主要从 IUserInterface 、 IQueryStatus两个方面入手,在Update中通过对IQueryStatus的判断来显示分数、局数和计时功能。
第三步,补充上述未完成的粒子碰撞部分。补充粒子爆炸部分
1 | using UnityEngine; |
2 | using UnityEngine.UI; |
3 | using System.Collections; |
4 | using Com.Mygame; |
5 | |
6 | public class UserInterface : MonoBehaviour { |
7 | |
8 | public Text mainText; |
9 | public Text scoreText; |
10 | public Text roundText; |
11 | |
12 | private int round; |
13 | |
14 | public GameObject bullet; |
15 | public ParticleSystem explosion; |
16 | |
17 | public float fireRate = .25f; |
18 | public float speed = 500f; |
19 | |
20 | private float nextFireTime; |
21 | |
22 | private IUserInterface userInt; |
23 | private IQueryStatus queryInt; |
24 | |
25 | |
26 | // Use this for initialization |
27 | void Start () { |
28 | bullet = GameObject.Instantiate (bullet) as GameObject; |
29 | explosion = GameObject.Instantiate (explosion) as ParticleSystem; |
30 | userInt = SceneController.getInstance () as IUserInterface; |
31 | queryInt = SceneController.getInstance () as IQueryStatus; |
32 | } |
33 | |
34 | // Update is called once per frame |
35 | void Update () { |
36 | if (queryInt.isCounting ()) { |
37 | mainText.text = ((int)queryInt.getEmitTime ()).ToString (); |
38 | } else { |
39 | if (Input.GetKeyDown (KeyCode.Space)) { |
40 | userInt.emitDisk (); |
41 | } |
42 | if (queryInt.isShooting ()) { |
43 | mainText.text = " "; |
44 | } else { |
45 | mainText.text = "Press space"; |
46 | } |
47 | if (queryInt.isShooting() && Input.GetMouseButtonDown (0) && Time.time > nextFireTime) { |
48 | nextFireTime = Time.time + fireRate; |
49 | |
50 | Ray ray = Camera.main.ScreenPointToRay (Input.mousePosition); |
51 | bullet.GetComponent<Rigidbody> ().velocity = Vector3.zero; |
52 | bullet.transform.position = transform.position; |
53 | bullet.GetComponent<Rigidbody> ().AddForce (ray.direction * speed, ForceMode.Impulse); |
54 | |
55 | RaycastHit hit; |
56 | if (Physics.Raycast (ray, out hit) && hit.collider.gameObject.tag == "Disk") { |
57 | explosion.transform.position = hit.collider.gameObject.transform.position; |
58 | explosion.GetComponent<Renderer> ().material.color = hit.collider.gameObject.GetComponent<Renderer> ().material.color; |
59 | explosion.Play (); |
60 | hit.collider.gameObject.SetActive (false); |
61 | } |
62 | } |
63 | } |
64 | roundText.text = " Round: " + queryInt.getRound ().ToString (); |
65 | scoreText.text = " Score: " + queryInt.getPoint ().ToString (); |
66 | |
67 | if (round != queryInt.getRound ()) { |
68 | round = queryInt.getRound (); |
69 | mainText.text = "Round: " + round.ToString() + "!"; |
70 | } |
71 | } |
72 | |
73 | } |
这样子就可以得到一个初步的demo进行试验了。
补充游戏规则–Judge计分系统
游戏规则单独作为一个类,有利于日后修改。这里需要处理的规则无非就两个,得分和失分。另外,得分需要判断是否能晋级下一关。能就调用接口函数nextRound()。
1 | using UnityEngine; |
2 | using System.Collections; |
3 | using Com.Mygame; |
4 | |
5 | public class Judge : MonoBehaviour { |
6 | public int oneDiskScore = 10; |
7 | public int oneDiskFail = 10; |
8 | public int disksToWin = 4; |
9 | |
10 | private SceneController scene; |
11 | |
12 | void Awake() { |
13 | scene = SceneController.getInstance(); |
14 | scene.setJudge(this); |
15 | } |
16 | |
17 | void Start() { |
18 | scene.nextRound(); // 默认开始第一关 |
19 | } |
20 | |
21 | // 击中飞碟得分 |
22 | public void scoreADisk() { |
23 | scene.setPoint(scene.getPoint() + oneDiskScore); |
24 | if (scene.getPoint() == disksToWin*oneDiskScore) { |
25 | scene.nextRound(); |
26 | } |
27 | } |
28 | |
29 | // 掉落飞碟失分 |
30 | public void failADisk() { |
31 | scene.setPoint(scene.getPoint() - oneDiskFail); |
32 | } |
33 | } |
接下来在场景控制器中添加相应裁判的代码
1 | // Com.Mygame内添加 |
2 | public interface IjudgeEvent { |
3 | void nextRound(); |
4 | void setPoint(int point); |
5 | } |
6 | // 类内部添加并且类继承IjudgeEvent |
7 | private Judge _judge; |
8 | public void setJudge(Judge obj) { _judge = obj; } |
9 | internal Judge getJudge() { return _judge; } |
在GameModel中调用裁判计分功能
1 | void Update () { |
2 | for (int i = 0; i < disks.Count; i++) { |
3 | if (!disks [i].activeInHierarchy) { |
4 | scene.getJudge ().scoreADisk (); |
5 | freeDisk (i); |
6 | } else if (disks [i].transform.position.y < 0) { |
7 | scene.getJudge ().failADisk (); |
8 | freeDisk (i); |
9 | } |
10 | } |
11 | |
12 | if (disks.Count == 0) { |
13 | shooting = false; |
14 | } |
15 | } |
设置关卡
在SceneControllerBaseCode中添加关卡信息,通过添加loadRoundData来完成每个关卡对游戏对象属性的设置。
1 | public void loadRoundData(int round) { |
2 | switch(round) { |
3 | case 1: // 第一关 |
4 | color = Color.green; |
5 | emitPos = new Vector3(-2.5f, 0.2f, -5f); |
6 | emitDir = new Vector3(24.5f, 40.0f, 67f); |
7 | speed = 4; |
8 | SceneController.getInstance().getGameModel().setting(1, color, emitPos, emitDir.normalized, speed, 1); |
9 | break; |
10 | case 2: // 第二关 |
11 | color = Color.red; |
12 | emitPos = new Vector3(2.5f, 0.2f, -5f); |
13 | emitDir = new Vector3(-24.5f, 35.0f, 67f); |
14 | speed = 4; |
15 | SceneController.getInstance().getGameModel().setting(1, color, emitPos, emitDir.normalized, speed, 2); |
16 | break; |
17 | case 3: // 第二关 |
18 | color = Color.yellow; |
19 | emitPos = new Vector3(2.5f, 0.2f, -5f); |
20 | emitDir = new Vector3(-24.5f, 35.0f, 67f); |
21 | speed = 4; |
22 | SceneController.getInstance().getGameModel().setting(1, color, emitPos, emitDir.normalized, speed, 3); |
23 | break; |
24 | } |
25 | } |
然后在SceneController补全nextRound函数
1 | public void nextRound() { |
2 | _point = 0; |
3 | _baseCode.loadRoundData(++_round); |
4 | } |
成果图
存在的问题
粒子爆炸的效果在第一次之后就不会有了,是否是一个粒子爆炸效果只能存活一次?或者是什么问题?