unity3d-射击飞碟小游戏
通过简单工厂和射线完成一个简单的设计飞碟小游戏
游戏要求
- 写个用鼠标打飞碟的游戏。
- 游戏要分多个 round , 飞碟数量每个 round 都是 n 个,但色彩,大小;发射位置,速度,角度,每次发射数量可以变化。
- 游戏过程中,仅能创建 n 个飞碟, 且不容许初始化阶段生成任何飞碟。 飞碟线路计算请使用 mathf 类。 向下加速度 a 是常数。 飞碟被用户击中,则回收。并按你定义的规则计算分数。飞碟落地(y < c),则自动回收。
- 请画 UML 图,描述主要对象及其关系。
说实话,不是很会画uml图,所以暂时先保留
游戏实现
这次的游戏可以在地面攻击目标的基础上进行开发,通过之前写好的简单工厂来回收和管理飞碟。至于游戏的分局主要就是通过添加一个UserInterface的脚本来进行选择,以及显示时间和分数。
简单工厂—管理飞碟
关于飞碟的生成和回收管理,我们一样主要通过简单工厂里的三个函数来进行管理。placeTarget完成在一个位置生成飞碟对象,内部通过调用GenGameObject的类来进行生成调度对象,ApplyPlaceTarget是在拿到游戏对象之后对其进行的具体操作。也是通过一个placeTargetAction的类来控制其动作的。hideMark就是将回收后的物体放回指定位置的函数。这就是在放置目标的脚本的基础上进行修改的。
此外,我还添加了score来记录分数,round来记录局数,state来记录游戏的状态。以及设置了相应的函数hitTarget()在击中飞碟对象之后分数+1,missTarget()未击中飞碟飞碟落地之后分数-1。以及相应的函数来获取这些值。
在AttackAction.cs中
1 | namespace Com.Mygame { |
2 | public enum State { OVER, PLAYING }; |
3 | |
4 | public class AttackManager : System.Object { |
5 | private static AttackManager _instance; |
6 | private GenGameObject _gen_game_obj; |
7 | private int score; |
8 | private int round; |
9 | public State state; |
10 | |
11 | void Start () { |
12 | score = 0; |
13 | } |
14 | public void hitTarget() { |
15 | score += 1; |
16 | } |
17 | public void missTarget() { |
18 | score -= 1; |
19 | } |
20 | |
21 | public int getScore () { |
22 | return score; |
23 | } |
24 | public void setRound (int r) { |
25 | round = r; |
26 | } |
27 | |
28 | public static AttackManager GetInstance(){ |
29 | if (_instance == null) { |
30 | _instance = new AttackManager(); |
31 | } |
32 | return _instance; |
33 | } |
34 | public GenGameObject getGenGameObject() { |
35 | return _gen_game_obj; |
36 | } |
37 | |
38 | internal void setGenGameObject(GenGameObject ggo) { |
39 | if (_gen_game_obj == null) { |
40 | _gen_game_obj = ggo; |
41 | } |
42 | } |
43 | |
44 | public GameObject placeTarget (Vector3 position){ |
45 | GameObject target = _gen_game_obj.genTarget (position); |
46 | if (target == null) |
47 | return null; |
48 | return ApplyPlaceTarget(target, position); |
49 | } |
50 | |
51 | public GameObject ApplyPlaceTarget (GameObject target, Vector3 position) { |
52 | if (target != null) { |
53 | placeTargetAction ac = target.AddComponent<placeTargetAction> (); |
54 | ac.setting (target, position); |
55 | ac.setRound (round); |
56 | forceManager fc = target.AddComponent<forceManager> (); |
57 | fc.setting (target); |
58 | } |
59 | return target; |
60 | } |
61 | public void hideMark (GameObject target) { |
62 | if (target != null) { |
63 | Debug.Log ("hide"); |
64 | target.transform.position = new Vector3 (0, 40, 0); |
65 | placeTargetAction ac = target.GetComponent<placeTargetAction> (); |
66 | ac.setRound (round); |
67 | |
68 | forceManager fc = target.AddComponent<forceManager> (); |
69 | fc.setting (target); |
70 | if (_gen_game_obj.getUsed ().Count > 0) { |
71 | _gen_game_obj.getUnused ().Enqueue (_gen_game_obj.getUsed ().Dequeue ()); |
72 | } |
73 | } |
74 | } |
75 | |
76 | } |
77 | public class placeTargetAction : MonoBehaviour { |
78 | private Vector3 position; |
79 | private GameObject target; |
80 | private int round; |
81 | |
82 | public void setting (GameObject target, Vector3 position){ |
83 | this.position = position; |
84 | this.target = target; |
85 | } |
86 | public void setRound(int round) { |
87 | this.round = round; |
88 | } |
89 | |
90 | void Start () { |
91 | transform.position = position; |
92 | } |
93 | |
94 | void Update () { |
95 | if (round == 1) { |
96 | target.GetComponent<Renderer> ().material.color = Color.red; |
97 | } else if (round == 2) { |
98 | target.GetComponent<Renderer> ().material.color = Color.blue; |
99 | } else if (round == 3) { |
100 | target.GetComponent<Renderer> ().material.color = Color.yellow; |
101 | } |
102 | } |
103 | |
104 | } |
105 | } |
创建调度飞碟对象
在简单工厂中所需要的游戏的对象,都是直接通过向GenGameObject这个类里面的方法来直接拿的。实现的主要方法也与放置目标的类似,当游戏对象不够而用户申请的时候就生成,之后就只在使用和未使用的游戏对象中进行通过队列进行调度,不再进一步生成或摧毁对象。
设置飞碟数目
在这里我设置飞碟数目num最多为10个并且在生成的时候会用if(used.Count + unused.Count < num)决策进行检测,设置为public在unity3d的视窗中也可以自己进行设置。
设置预设
这里我就使用一个圆筒来代替飞碟,通过材质球来添加颜色,不添加也行因为在关卡部分也会进行设置。
1 | using UnityEngine; |
2 | using System.Collections; |
3 | using System.Collections.Generic; |
4 | using Com.Mygame; |
5 | |
6 | |
7 | public class GenGameObject : MonoBehaviour { |
8 | public GameObject prefab; |
9 | public int num = 10; // target number |
10 | |
11 | Queue<GameObject> unused = new Queue<GameObject>(); |
12 | Queue<GameObject> used = new Queue<GameObject>(); |
13 | |
14 | // Use this for initialization |
15 | void Start () { |
16 | AttackManager.GetInstance ().setGenGameObject (this); |
17 | } |
18 | |
19 | public GameObject genTarget(Vector3 position) { |
20 | GameObject target; |
21 | if (used.Count + unused.Count < num) { |
22 | if (unused.Count == 0) { |
23 | target = Instantiate (prefab, position, Quaternion.identity) as GameObject; |
24 | used.Enqueue (target); |
25 | } else { |
26 | target = unused.Dequeue (); |
27 | } |
28 | } else { |
29 | target = null; |
30 | } |
31 | return target; |
32 | } |
33 | public Queue<GameObject> getUnused() { |
34 | return unused; |
35 | } |
36 | public Queue<GameObject> getUsed() { |
37 | return used; |
38 | } |
39 | // Update is called once per frame |
40 | void Update () { |
41 | |
42 | } |
43 | } |
用户交互
主要添加一个UserInterface.cs来进行对游戏关卡的设置还有分数和时间的显示。分数的计算,就直接在简单工厂中进行会来的比较方便,这里只需要获得工厂对象再将它的分数显示出来就可以了。以及交互对局数和状态的设置。
界面上就可以看到这种效果。
1 | using UnityEngine; |
2 | using System.Collections; |
3 | using System.Collections.Generic; |
4 | using Com.Mygame; |
5 | |
6 | public class UserInterface : MonoBehaviour { |
7 | |
8 | private float time; |
9 | |
10 | AttackManager my; |
11 | |
12 | // Use this for initialization.. |
13 | void Start () { |
14 | my = AttackManager.GetInstance(); |
15 | my.setRound(1); |
16 | time = 30; |
17 | my.state = State.OVER; |
18 | } |
19 | |
20 | // Update is called once per frame |
21 | void Update () { |
22 | if (time > 0) { |
23 | time -= Time.deltaTime; |
24 | } else { |
25 | my.state = State.OVER; |
26 | } |
27 | } |
28 | |
29 | void OnGUI() { |
30 | if (GUILayout.Button("level1")) { |
31 | my.setRound(1); |
32 | my.state = State.PLAYING; |
33 | } |
34 | if (GUILayout.Button("level2")) { |
35 | my.setRound(2); |
36 | my.state = State.PLAYING; |
37 | } |
38 | if (GUILayout.Button("level3")) { |
39 | my.setRound(3); |
40 | my.state = State.PLAYING; |
41 | } |
42 | |
43 | GUILayout.Label ("Score: " + my.getScore()); |
44 | GUILayout.Label ("time: " + Mathf.Ceil(time)); |
45 | |
46 | } |
47 | } |
Ray射线—打飞碟
完成以上的步骤之后,我们已经可以调度好飞碟对象,并且有用户交互的界面了。现在最重要的就是需要一个方法把飞碟打下来。
在我的理解中,打飞碟的方法应该有两种,一种是用子弹作为碰撞体来碰撞飞碟触发条件。一种就是直接用射线来完成触发条件。由于暂时还未加入对子弹,所以我暂时使用Ray射线来碰撞飞碟。通过简单修改放置攻击目标的代码就可以完成了。
这里将预设的标签设置为target,就只有在射线碰撞到有这个标签的游戏对象之后才会触发隐藏游戏对象的条件还有得分方法。
1 | public class AttackAction : MonoBehaviour { |
2 | |
3 | private GameObject cam; |
4 | private AttackManager myFactory; |
5 | |
6 | // Use this for initialization |
7 | void Start () { |
8 | cam = GameObject.Find ("Main Camera"); |
9 | |
10 | myFactory = AttackManager.GetInstance (); |
11 | |
12 | } |
13 | void Update () { |
14 | myFactory.placeTarget (new Vector3(0, 40, 0)); |
15 | if (Input.GetMouseButtonDown (0)) { |
16 | |
17 | //create ray, origin is camera, and direction to mousepoint |
18 | Ray ray = cam.GetComponent<Camera>().ScreenPointToRay(Input.mousePosition); |
19 | |
20 | //Return the ray's hit |
21 | RaycastHit hit; |
22 | if (Physics.Raycast (ray, out hit)) { |
23 | print (hit.transform.gameObject.name); |
24 | print (hit.point); |
25 | if (hit.collider.gameObject.tag.Contains ("target")) { |
26 | myFactory.hideMark (hit.collider.gameObject); |
27 | myFactory.hitTarget (); |
28 | print ("hit"); |
29 | } |
30 | } |
31 | } |
32 | } |
33 | } |
碰撞体—回收飞碟
除了通过ray打掉的飞碟之后,还会有一部分的飞碟是掉落到地板上的。这时候我们就需要把飞碟的预设设置为碰撞体,地板也设置为碰撞体,并且会触发碰撞条件。在飞碟碰撞到地板之后触发条件,进行回收和扣分。
ufo
plane
这个时候我们可以在plane上挂在一段新的代码用作触发函数。other就是与地面碰撞的地板。或者直接将这段脚本加入AttackAction中。
1 | void OnTriggerEnter(Collider other){ |
2 | Destroy (other.GetComponent<forceManager> ()); |
3 | myFactory.hideMark (other.gameObject); |
4 | myFactory.missTarget (); |
5 | } |
刚体—飞碟受力
要使对象受物理引擎控制,简单的办法就是给它加个 Rigidbody ,对象就会受到重力影响,而且会和世界中的其他对象碰撞。
所以为了完成飞碟的碰撞和掉落以及向不同方向飞,我们还需要给他加上刚体部件才能够顺利受力力完成碰撞和掉落。
在Com.Mygame中加入,forceManager来管理受力,使每次发射出去都受到不同方向的力就能使飞碟不同方向地飞。
1 | public class forceManager : MonoBehaviour { |
2 | private GameObject target; |
3 | private Rigidbody rb; |
4 | private int force; |
5 | public void setting (GameObject target) { |
6 | this.target = target; |
7 | } |
8 | void Start () { |
9 | rb = target.GetComponent<Rigidbody> (); |
10 | force = Random.Range (-5, 5); |
11 | } |
12 | void Update () { |
13 | rb.AddForce (new Vector3 (force, 0), ForceMode.Force); |
14 | } |
15 | } |
在上面的OnTriggerEnter函数中,触发之后就会删除这个部件,下一次重新调度之后 ApplyPlaceTarget中来又重新添加上。这样子就可以保证上以不同的方向出发。
游戏效果
三种颜色代表了不同的关卡,整个游戏过程中只生成了10个游戏对象。当然界面有一点丑,整体脚本已经写好。可以进一步地进行优化。
脚本
AttackAction.cs
1 | using UnityEngine; |
2 | using System.Collections; |
3 | using System.Collections.Generic; |
4 | using Com.Mygame; |
5 | |
6 | namespace Com.Mygame { |
7 | public enum State { OVER, PLAYING }; |
8 | |
9 | public class AttackManager : System.Object { |
10 | private static AttackManager _instance; |
11 | private GenGameObject _gen_game_obj; |
12 | private int score; |
13 | private int round; |
14 | public State state; |
15 | |
16 | void Start () { |
17 | score = 0; |
18 | } |
19 | public void hitTarget() { |
20 | score += 1; |
21 | } |
22 | public void missTarget() { |
23 | score -= 1; |
24 | } |
25 | |
26 | public int getScore () { |
27 | return score; |
28 | } |
29 | public void setRound (int r) { |
30 | round = r; |
31 | } |
32 | |
33 | public static AttackManager GetInstance(){ |
34 | if (_instance == null) { |
35 | _instance = new AttackManager(); |
36 | } |
37 | return _instance; |
38 | } |
39 | public GenGameObject getGenGameObject() { |
40 | return _gen_game_obj; |
41 | } |
42 | |
43 | internal void setGenGameObject(GenGameObject ggo) { |
44 | if (_gen_game_obj == null) { |
45 | _gen_game_obj = ggo; |
46 | } |
47 | } |
48 | |
49 | public GameObject placeTarget (Vector3 position){ |
50 | GameObject target = _gen_game_obj.genTarget (position); |
51 | if (target == null) |
52 | return null; |
53 | return ApplyPlaceTarget(target, position); |
54 | } |
55 | |
56 | public GameObject ApplyPlaceTarget (GameObject target, Vector3 position) { |
57 | if (target != null) { |
58 | placeTargetAction ac = target.AddComponent<placeTargetAction> (); |
59 | ac.setting (target, position); |
60 | ac.setRound (round); |
61 | forceManager fc = target.AddComponent<forceManager> (); |
62 | fc.setting (target); |
63 | } |
64 | return target; |
65 | } |
66 | public void hideMark (GameObject target) { |
67 | if (target != null) { |
68 | Debug.Log ("hide"); |
69 | target.transform.position = new Vector3 (0, 40, 0); |
70 | placeTargetAction ac = target.GetComponent<placeTargetAction> (); |
71 | ac.setRound (round); |
72 | |
73 | forceManager fc = target.AddComponent<forceManager> (); |
74 | fc.setting (target); |
75 | if (_gen_game_obj.getUsed ().Count > 0) { |
76 | _gen_game_obj.getUnused ().Enqueue (_gen_game_obj.getUsed ().Dequeue ()); |
77 | } |
78 | } |
79 | } |
80 | |
81 | } |
82 | |
83 | public class placeTargetAction : MonoBehaviour { |
84 | private Vector3 position; |
85 | private GameObject target; |
86 | private int round; |
87 | |
88 | public void setting (GameObject target, Vector3 position){ |
89 | this.position = position; |
90 | this.target = target; |
91 | } |
92 | public void setRound(int round) { |
93 | this.round = round; |
94 | } |
95 | |
96 | void Start () { |
97 | transform.position = position; |
98 | } |
99 | |
100 | void Update () { |
101 | if (round == 1) { |
102 | target.GetComponent<Renderer> ().material.color = Color.red; |
103 | } else if (round == 2) { |
104 | target.GetComponent<Renderer> ().material.color = Color.blue; |
105 | } else if (round == 3) { |
106 | target.GetComponent<Renderer> ().material.color = Color.yellow; |
107 | } |
108 | } |
109 | |
110 | } |
111 | public class forceManager : MonoBehaviour { |
112 | private GameObject target; |
113 | private Rigidbody rb; |
114 | private int force; |
115 | public void setting (GameObject target) { |
116 | this.target = target; |
117 | } |
118 | void Start () { |
119 | rb = target.GetComponent<Rigidbody> (); |
120 | force = Random.Range (-5, 5); |
121 | } |
122 | void Update () { |
123 | rb.AddForce (new Vector3 (force, 0), ForceMode.Force); |
124 | } |
125 | } |
126 | |
127 | } |
128 | |
129 | public class AttackAction : MonoBehaviour { |
130 | |
131 | private GameObject cam; |
132 | private AttackManager myFactory; |
133 | |
134 | // Use this for initialization |
135 | void Start () { |
136 | cam = GameObject.Find ("Main Camera"); |
137 | |
138 | myFactory = AttackManager.GetInstance (); |
139 | |
140 | } |
141 | void Update () { |
142 | myFactory.placeTarget (new Vector3(0, 40, 0)); |
143 | if (Input.GetMouseButtonDown (0)) { |
144 | |
145 | //create ray, origin is camera, and direction to mousepoint |
146 | Ray ray = cam.GetComponent<Camera>().ScreenPointToRay(Input.mousePosition); |
147 | |
148 | //Return the ray's hit |
149 | RaycastHit hit; |
150 | if (Physics.Raycast (ray, out hit)) { |
151 | print (hit.transform.gameObject.name); |
152 | print (hit.point); |
153 | if (hit.collider.gameObject.tag.Contains ("target")) { |
154 | myFactory.hideMark (hit.collider.gameObject); |
155 | myFactory.hitTarget (); |
156 | print ("hit"); |
157 | } |
158 | } |
159 | } |
160 | } |
161 | void OnTriggerEnter(Collider other){ |
162 | Destroy (other.GetComponent<forceManager> ()); |
163 | myFactory.hideMark (other.gameObject); |
164 | myFactory.missTarget (); |
165 | } |
166 | |
167 | } |
GenGameObject.cs
1 | using UnityEngine; |
2 | using System.Collections; |
3 | using System.Collections.Generic; |
4 | using Com.Mygame; |
5 | |
6 | |
7 | public class GenGameObject : MonoBehaviour { |
8 | public GameObject prefab; |
9 | public int num = 10; // target number |
10 | |
11 | Queue<GameObject> unused = new Queue<GameObject>(); |
12 | Queue<GameObject> used = new Queue<GameObject>(); |
13 | |
14 | // Use this for initialization |
15 | void Start () { |
16 | AttackManager.GetInstance ().setGenGameObject (this); |
17 | } |
18 | |
19 | public GameObject genTarget(Vector3 position) { |
20 | GameObject target; |
21 | if (used.Count + unused.Count < num) { |
22 | if (unused.Count == 0) { |
23 | target = Instantiate (prefab, position, Quaternion.identity) as GameObject; |
24 | used.Enqueue (target); |
25 | } else { |
26 | target = unused.Dequeue (); |
27 | } |
28 | } else { |
29 | target = null; |
30 | } |
31 | return target; |
32 | } |
33 | public Queue<GameObject> getUnused() { |
34 | return unused; |
35 | } |
36 | public Queue<GameObject> getUsed() { |
37 | return used; |
38 | } |
39 | // Update is called once per frame |
40 | void Update () { |
41 | |
42 | } |
43 | } |
UserInterface.cs
1 | using UnityEngine; |
2 | using System.Collections; |
3 | using System.Collections.Generic; |
4 | using Com.Mygame; |
5 | |
6 | public class UserInterface : MonoBehaviour { |
7 | |
8 | private float time; |
9 | |
10 | AttackManager my; |
11 | |
12 | // Use this for initialization.. |
13 | void Start () { |
14 | my = AttackManager.GetInstance(); |
15 | my.setRound(1); |
16 | time = 30; |
17 | my.state = State.OVER; |
18 | } |
19 | |
20 | // Update is called once per frame |
21 | void Update () { |
22 | if (time > 0) { |
23 | time -= Time.deltaTime; |
24 | } else { |
25 | my.state = State.OVER; |
26 | } |
27 | } |
28 | |
29 | void OnGUI() { |
30 | if (GUILayout.Button("level1")) { |
31 | my.setRound(1); |
32 | my.state = State.PLAYING; |
33 | } |
34 | if (GUILayout.Button("level2")) { |
35 | my.setRound(2); |
36 | my.state = State.PLAYING; |
37 | } |
38 | if (GUILayout.Button("level3")) { |
39 | my.setRound(3); |
40 | my.state = State.PLAYING; |
41 | } |
42 | |
43 | GUILayout.Label ("Score: " + my.getScore()); |
44 | GUILayout.Label ("time: " + Mathf.Ceil(time)); |
45 | |
46 | } |
47 | } |