unity3d-射击飞碟改进版

unity3d-射击飞碟改进版

本文章参考Unity3D学习笔记(6)—— 飞碟射击游戏 胡江川同学的博客,根据他的思路重新进行实践学习,并且加以理解修改整理为如下博文。有需要可以参考原文,写的详细又清楚。

题目要求

编制一个射飞碟游戏。

具体要求如下:

  1. 假设有一支枪在摄像机位置(0,1,-10),在(0,0,0-10-20)放置三个小球作为距离标记,调整视角直到小球在下中部
  2. 将鼠标所在平面坐标,转换为子弹(球体)射出的角度方向。子弹使用物理引擎,初速度恒定。
  3. 游戏要分多个 round , 飞碟数量每个 round 都是 n 个,但色彩,大小;发射位置,速度,角度,每次发射数量按预定规则变化。
  4. 用户按空格后,321倒数3秒,飞碟飞出(物理引擎控制),点击鼠标,子弹飞出。飞碟落地,或被击中,则准备下一次射击。
  5. 以下是一些技术要求:
    • 子弹仅需要一个,不用时处于 deactive 状态
    • 飞碟用一个带缓存的工厂生产,template 中放置预制的飞碟对象
    • 程序类图设计大致如下:

步骤

设置摄像机和预设

通过屏幕的调整来设置摄像机的参数:

摄像机:

子弹:

飞碟:

粒子效果:

脚本实现子弹射击

脚本挂在在摄像机上

子弹射击的思路:当用户点击鼠标时,从摄像机到鼠标创建一条射线,射线的方向即是子弹发射的方向,子弹采用刚体组件,因此发射子弹只需要给子弹施加一个力。子弹对象只有一个,下一次发射子弹时,必须改变子弹的位置(虽然有了刚体组件不建议修改transform,但也没有其它方法改变子弹位置了吧)。为了不让子弹继承上一次发射的速度,必须将子弹的速度归零重置。

子弹的击中判断:采用射线而不是物理引擎,因为物理引擎在高速物体碰撞时经常不能百分百检测得到。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
using UnityEngine;
using UnityEngine.UI;
using System.Collections;
public class UserInterface : MonoBehaviour {
public GameObject bullet;
public float fireRate = .25f;
public float speed = 500f;
private float nextFireTime;
// Use this for initialization
void Start () {
bullet = GameObject.Instantiate (bullet) as GameObject;
}
// Update is called once per frame
void Update () {
if (Input.GetMouseButtonDown (0)) {
nextFireTime = Time.time + fireRate;
Ray ray = Camera.main.ScreenPointToRay (Input.mousePosition);
bullet.GetComponent<Rigidbody>().velocity = Vector3.zero; // 速度设为0
bullet.transform.position = transform.position; // 脚本挂载在摄像机上,子弹从摄像机位置飞出
bullet.GetComponent<Rigidbody>().AddForce(ray.direction*speed, ForceMode.Impulse); // 给子弹添加力
}
}
}

运行之后,发现子弹可以从摄像机的方向射出了。

完成飞碟工厂

创建新的命名空间Com.Mygame,单例类DiskFactory和SceneController都定义其中。飞碟工厂类的目的是管理飞碟实例,同时对外屏蔽飞碟实例的的提取和回收细节,对于需要使用飞碟的其他对象,只能使用工厂类提供的3个函数,分别是getDisk()、getDiskObject()、free()。

有几点需要注意:

  1. 当且仅当请求队列里的所有对象都在被使用(飞碟在场景中活跃)时,才会发生实例化,此时队列会变长。
  2. getDisk返回的是可用飞碟在队列里的index,这是为了方便free。
  3. free通过index找到飞碟在队列中的位置,并将飞碟设置为不活跃的。注意,由于飞碟使用了刚体组件,回收时需要把速度重置,并且大小可能会被改变,也应该重置。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
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 diskPrefab;
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(diskPrefab) as GameObject);
return diskList.Count-1;
}
// 获取飞碟对象
public GameObject getDiskObject(int id) {
if (id >= 0 && id < diskList.Count) {
return diskList[id];
}
return null;
}
// 回收飞碟
public void free(int id) {
if (id >= 0 && id < diskList.Count) {
// 重置飞碟速度
diskList[id].GetComponent<Rigidbody>().velocity = Vector3.zero;
// 重置飞碟大小
diskList[id].transform.localScale = diskPrefab.transform.localScale;
diskList[id].SetActive(false);
}
}
}
}
public class DiskFactoryBasecode : MonoBehaviour {
public GameObject disk;
void Awake () {
// 初始化预设对象
DiskFactory.getInstance().diskPrefab = disk;
}
}

完成游戏场景

场景类是整个飞碟射击游戏的核心类,主要负责飞碟动作的处理。胡同学是这样设计的:首先需要倒计时功能,可以通过几个整型变量和布尔变量完成。另外需要飞碟发射功能,通过setting函数保存好飞碟的发射信息,每次倒计时完成后,通过emitDisks获取飞碟对象,并通过发射信息初始化飞碟,再给飞碟一个力就可以发射了。而飞碟的回收在Update里完成,一种是飞碟被击中(飞碟不在场景中)了,需要调用Judge获得分数。另一种是飞碟在场景中,但是掉在地上了,需要调用Judge丢失分数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
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> ();
private int diskScale;
private Color diskColor;
private Vector3 emitPosition;
private Vector3 emitDirection;
private float emitSpeed;
private int emitNumber;
private bool emitEnable;
void Awake() {
}
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].GetComponent<Renderer> ().material.color = diskColor;
disks [i].transform.position = new Vector3 (emitPosition.x, emitPosition.y + i, emitPosition.z);
disks [i].SetActive (true);
disks [i].GetComponent<Rigidbody> ().AddForce (emitDirection * Random.Range (emitSpeed * 5, emitSpeed * 10) / 10, ForceMode.Impulse);
}
}
void freeDisk(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;
}
}
}
// Use this for initialization
void Start () {
}
// Update is called once per frame
void Update () {
for (int i = 0; i < disks.Count; i++) {
if (!disks [i].activeInHierarchy) {
freeDisk (i);
} else if (disks [i].transform.position.y < 0) {
freeDisk (i);
}
}
if (disks.Count == 0) {
shooting = false;
}
}
}

场景控制器

场景控制类主要实现接口定义和保存注入对象。另外它有两个私有变量round和point,分别记录游戏正在进行的回合,以及玩家目前的得分。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
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 class SceneController : System.Object, IQueryStatus, IUserInterface {
private static SceneController _instance;
private GameModel _gameModel;
private SceneControllerBaseCode _baseCode;
private int _round;
private int _point;
public static SceneController getInstance() {
if (_instance == null) {
_instance = new SceneController ();
}
return _instance;
}
public void setSceneControllerBaseCode (SceneControllerBaseCode obj) {
_baseCode = obj;
}
internal SceneControllerBaseCode getSceneControllerBC() {
return _baseCode;
}
public void setGameModel(GameModel obj) {
_gameModel = obj;
}
// 当前程序或派生类可用
internal GameModel getGameModel() {
return _gameModel;
}
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;
}
}
}
public class SceneControllerBaseCode : MonoBehaviour {
private Color color;
private Vector3 emitPos;
private Vector3 emitDir;
private float speed;
void Awake() {
SceneController.getInstance().setSceneControllerBaseCode(this);
}
void Start() {
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);
}
}

完成了目前的场景控制器之后,在GameModel中添加场景控制器

1
2
3
4
5
6
private SceneController scene;
void Awake() {
scene = SceneController.getInstance ();
scene.setGameModel (this);
}

完善UserInterface

第一步, 在场景编辑器里右键创建UI元素Text,调整 text 对齐位置和文字大小即可。

第二步,对UserInterface的完善主要从 IUserInterface 、 IQueryStatus两个方面入手,在Update中通过对IQueryStatus的判断来显示分数、局数和计时功能。

第三步,补充上述未完成的粒子碰撞部分。补充粒子爆炸部分

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
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;
// Use this for initialization
void Start () {
bullet = GameObject.Instantiate (bullet) as GameObject;
explosion = GameObject.Instantiate (explosion) as ParticleSystem;
userInt = SceneController.getInstance () as IUserInterface;
queryInt = SceneController.getInstance () as IQueryStatus;
}
// Update is called once per frame
void Update () {
if (queryInt.isCounting ()) {
mainText.text = ((int)queryInt.getEmitTime ()).ToString ();
} else {
if (Input.GetKeyDown (KeyCode.Space)) {
userInt.emitDisk ();
}
if (queryInt.isShooting ()) {
mainText.text = " ";
} else {
mainText.text = "Press space";
}
if (queryInt.isShooting() && Input.GetMouseButtonDown (0) && Time.time > nextFireTime) {
nextFireTime = Time.time + fireRate;
Ray ray = Camera.main.ScreenPointToRay (Input.mousePosition);
bullet.GetComponent<Rigidbody> ().velocity = Vector3.zero;
bullet.transform.position = transform.position;
bullet.GetComponent<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.GetComponent<Renderer> ().material.color = hit.collider.gameObject.GetComponent<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() + "!";
}
}
}

这样子就可以得到一个初步的demo进行试验了。

补充游戏规则–Judge计分系统

游戏规则单独作为一个类,有利于日后修改。这里需要处理的规则无非就两个,得分和失分。另外,得分需要判断是否能晋级下一关。能就调用接口函数nextRound()。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
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);
}
}

接下来在场景控制器中添加相应裁判的代码

1
2
3
4
5
6
7
8
9
// Com.Mygame内添加
public interface IjudgeEvent {
void nextRound();
void setPoint(int point);
}
// 类内部添加并且类继承IjudgeEvent
private Judge _judge;
public void setJudge(Judge obj) { _judge = obj; }
internal Judge getJudge() { return _judge; }

在GameModel中调用裁判计分功能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void Update () {
for (int i = 0; i < disks.Count; i++) {
if (!disks [i].activeInHierarchy) {
scene.getJudge ().scoreADisk ();
freeDisk (i);
} else if (disks [i].transform.position.y < 0) {
scene.getJudge ().failADisk ();
freeDisk (i);
}
}
if (disks.Count == 0) {
shooting = false;
}
}

设置关卡

在SceneControllerBaseCode中添加关卡信息,通过添加loadRoundData来完成每个关卡对游戏对象属性的设置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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;
case 3: // 第二关
color = Color.yellow;
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, 3);
break;
}
}

然后在SceneController补全nextRound函数

1
2
3
4
public void nextRound() {
_point = 0;
_baseCode.loadRoundData(++_round);
}

成果图

存在的问题

粒子爆炸的效果在第一次之后就不会有了,是否是一个粒子爆炸效果只能存活一次?或者是什么问题?

您的支持将鼓励我继续创作!