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
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()。

有几点需要注意:

  1. 当且仅当请求队列里的所有对象都在被使用(飞碟在场景中活跃)时,才会发生实例化,此时队列会变长。
  2. getDisk返回的是可用飞碟在队列里的index,这是为了方便free。
  3. 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
}

成果图

存在的问题

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

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