太空射击游戏

思考并回答以下问题:

本章要开发的游戏是一个使用双轴(Twin stick)模式的太空射击游戏。双轴模式的游戏是指游戏玩家通过两个维度或者轴的输入来控制运动范围,一个轴负责运动,另一个轴负责角度。本章将会包括以下重要主题:

  • 创建操作与预设体
  • 双轴控制以及轴向运动
  • 玩家角色控制器与射击机制
  • 基本的敌人运动与人工智能(AI)

导入资源

点击下载资源

跟导入贴图一样,Unity导入声音文件时也使用了以一些默认的参数。这些参数十分适合那些时长很短的音效,例如脚步声、枪声和爆炸声。但是如果导入的是时长较长的声音,例如乐曲,这些参数就可能出现问题,造成加载的时间过长。为了修复这个问题,可能在Project面板中选中音轨,然后在Inspector面板中,取消“Preload Audio Data”复选框的选中状态,最后在“Load Type”下拉列表框中选中“Streaming”选项,如图1所示。这样就可以确保音轨是流式传输的,而不需要在关卡启动时就完全加载到内存中。

PlayerController.cs

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
using UnityEngine;
using System.Collections;

public class PlayerController : MonoBehaviour
{
private Rigidbody ThisBody = null;
private Transform ThisTransform = null;

public bool MouseLook = true;
public string HorzAxis = "Horizontal";
public string VertAxis = "Vertical";
public string FireAxis = "Fire1";

public float MaxSpeed = 5f;

void Awake()
{
ThisBody = GetComponent<Rigidbody>();
ThisTransform = GetComponent<Transform>();
}

void FixedUpdate()
{
float Horz = Input.GetAxis(HorzAxis);
float Vert = Input.GetAxis(VertAxis);
Vector3 MoveDirection = new Vector3(Horz, 0.0f, Vert);

ThisBody.AddForce(MoveDirection.normalized * MaxSpeed);

ThisBody.velocity = new Vector3(
Mathf.Clamp(ThisBody.velocity.x, -MaxSpeed, MaxSpeed),
Mathf.Clamp(ThisBody.velocity.y, -MaxSpeed, MaxSpeed),
Mathf.Clamp(ThisBody.velocity.z, -MaxSpeed, MaxSpeed)
);

if(MouseLook)
{
Vector3 MousePosWorld = Camera.main.ScreenToWorldPoint(new Vector3(Input.mousePosition.x, Input.mousePosition.y, 0.0f));

MousePosWorld = new Vector3(MousePosWorld.x, 0.0f, MousePosWorld.z);

Vector3 LookDirection = MousePosWorld - ThisTransform.position;

ThisTransform.localRotation = Quaternion.LookRotation(LookDirection.normalized, Vector3.up);
}
}
}

BoundsLock.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
using UnityEngine;
using System.Collections;

public class BoundsLock : MonoBehaviour
{
private Transform ThisTransform = null;
public Vector2 HorzRange = Vector2.zero;
public Vector2 VertRange = Vector2.zero;

void Awake()
{
ThisTransform = GetComponent<Transform>();
}

void LateUpdate()
{
ThisTransform.position = new Vector3
(
Mathf.Clamp(ThisTransform.position.x, HorzRange.x, HorzRange.y),
ThisTransform.position.y,
Mathf.Clamp(ThisTransform.position.z, VertRange.x, VertRange.y)
);
}
}

Health.cs

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
using UnityEngine;
using System.Collections;

public class Health : MonoBehaviour
{
public GameObject DeathParticlesPrefab = null;
private Transform ThisTransform = null;
public bool ShouldDestroyOnDeath = true;

[SerializeField]
private float _HealthPoints = 1000f;

void Start()
{
ThisTransform = GetComponent<Transform>();
}

public float HealthPoints()
{
get
{
return _HealthPoints;
}
set
{
_HealthPoints = value;

if (_HealthPoints <= 0)
{
SendMessage("Die", SendMessageOptions.DontRequireReceiver);

if(DeathParticlesPrefab != null)
Instantiate(DeathParticlesPrefab, ThisTransform.position, ThisTransform.rotation);

if(ShouldDestroyOnDeath)
Destroy(gameObject);
}
}
}
}

TimeDestroy.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
using UnityEngine;
using System.Collections;

public class TimeDestroy : MonoBehaviour
{
public float DestroyTime = 2f;

void Start()
{
Invoke("Die", DestroyTime);
}

void Die()
{
Destroy(gameObject);
}
}
1
2
3
4
5
6
7
8
9
10
public class Health : MonoBehaviour
{
...
void Update()
{
if (Input.GetKeyDown(KeyCode.Space))
HealthPoints = 0;
}
...
}

Mover.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
using UnityEngine;
using System.Collections;

public class Mover : MonoBehaviour
{
private Transform ThisTransform = null;
public float MaxSpeed = 10f;

void Awake()
{
ThisTransform = GetComponent<Transform>();
}

void Update()
{
ThisTransform.position += ThisTransform.forward * MaxSpeed * Time.deltaTime;
}
}

ObjFace.cs

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
using UnityEngine;
using System.Collections;

public class ObjFace : MonoBehaviour
{
public Transform ObjToFollow = null;
public bool FollowPlayer = false;
private Transform ThisTransform = null;

void Awake()
{
ThisTransform = GetComponent<Transform>();

if(!FollowPlayer) return;

GameObject PlayerObj = GameObject.FindGameObjectWithTag("Player");

if(PlayerObj != null)
ObjToFollow = PlayerObj.GetComponent<Transform>();
}

void Update()
{
if(ObjToFollow == null) return;

Vector3 DirToObject = ObjToFollow.position - ThisTransform.position;

if(DirToObject != Vector3.zero)
ThisTransform.localRotation = Quaternion.LookRotation(DirToObject.normalized, Vector3.up);
}
}

ProxyDamage.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
using UnityEngine;
using System.Collections;

public class ProxyDamage : MonoBehaviour
{

private float DamageRate = 10f;

void OnTriggerStay(Collider Col)
{
Health h = Col.gameObject.GetComponent<Health>();

if (H == null) return;

H.HealthPoints -= DamageRate * Time.deltaTime;
}
}

批量产生敌人

Spawner.cs

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
using UnityEngine;
using System.Collections;

public class Spawner : MonoBehaviour
{
public float MaxRadius = 1f;
public float Interval = 5f;
public GameObject ObjToSpawn = null;

private Transform Origin = null;

void Awake()
{
Origin = GameObject.FindGameObjectWithTag("Player").GetComponent<Transform>();
}

void Start()
{
InvokeRepeating("Spawn", 0f, Interval);
}

void Spawn()
{
if(Origin == null) return;

Vector3 SpawnPos = Origin.position + Random.onUnitSphere * MaxRadius;
SpawnPos = new Vector3(SpawnPos.x, 0f, SpawnPos.z);
Instantiate(ObjToSpawn, SpawnPos, Quaternion.identity);
}
}

炮弹预设体

Ammmo.cs

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
using UnityEngine;
using System.Collections;

public class Ammmo : MonoBehaviour
{
public float Damage = 100f;
public float LifeTime = 2f;

void OnEnable()
{
CancelInvoke();
Invoke("Die", LifeTime);
}

void OnTriggerEnter(Collider Col)
{
// 获取Health脚本组件
Health H = Col.gameObject.GetComponent<Health>();

if (H == null) return;

H.HealthPoints -= Damage;
}

void Die()
{
gameObject.SetActive(false);
}
}

炮弹的产生

AmmoManager.cs

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
using UnityEngine;
using System.Collections;
using System.Collections.Generic;

public class AmmoManager : MonoBehaviour
{
// 对ammo预设体的引用
public GameObject AmmmoPrefab = null;

// Ammo池计数
public int PoolSize = 100;

public Queue<Transform> AmmoQueue = new Queue<Transform>();

// 产生一个ammo对象的队列
private GameObject[] AmmmoArray;

// 静态字段只在堆中有一个,被所有实例化对象共享。
// 静态成员即使没有类的实例也存在。
public static AmmoManager AmmoManagerSingleton = null;

void Awake()
{
if(AmmoManagerSingleton != null)
{
Destroy(GetComponent<AmmoManager>());
return;
}

AmmoManagerSingleton = this;
AmmmoArray = new GameObject[PoolSize];

for(int i=0; i < PoolSize; i++)
{
AmmmoArray[i] = Instantiate(AmmmoPrefab, Vector3.zero, Quaternion.identity) as GameObject;

Transform ObjTransform = AmmmoArray[i].GetComponent<Transform>();

ObjTransform.parent = GetComponent<Transform>();
// 入列,100个Transform类的实例对象
AmmoQueue.Enqueue(ObjTransform);

AmmmoArray[i].SetActive(false);
}
}

public static Transform SpawnAmmo(Vector3 Position, Quaternion Rotation)
{
// 获取ammo,出列
Transform SpawnedAmmo = AmmoManagerSingleton.AmmoQueue.Dequeue();

SpawnedAmmo.gameObject.SetActive(true);
SpawnedAmmo.position = Position;
SpawnedAmmo.localRotation = Rotation;

// 添加到Queue尾部
AmmoManagerSingleton.AmmoQueue.Enqueue(SpawnedAmmo);

// 返回ammo
return SpawnedAmmo;
}
}

PlayerController.cs

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
using UnityEngine;
using System.Collections;

public class PlayerController : MonoBehaviour
{
private Rigidbody ThisBody = null;
private Transform ThisTransform = null;

public bool MouseLook = true;
public string HorzAxis = "Horizontal";
public string VertAxis = "Vertical";
public string FireAxis = "Fire1";

public float MaxSpeed = 5f;

public float ReloadDelay = 5f;
public bool CanFire = true;

public Transform[] TurretTransforms;

void Awake()
{
ThisBody = GetComponent<Rigidbody>();
ThisTransform = GetComponent<Transform>();
}

void FixedUpdate()
{
float Horz = Input.GetAxis(HorzAxis);
float Vert = Input.GetAxis(VertAxis);
Vector3 MoveDirection = new Vector3(Horz, 0.0f, Vert);

ThisBody.AddForce(MoveDirection.normalized * MaxSpeed);

// 对速度进行限制
ThisBody.velocity = new Vector3(
Mathf.Clamp(ThisBody.velocity.x, -MaxSpeed, MaxSpeed),
Mathf.Clamp(ThisBody.velocity.y, -MaxSpeed, MaxSpeed),
Mathf.Clamp(ThisBody.velocity.z, -MaxSpeed, MaxSpeed),
);

// 鼠标控制
if(MouseLook)
{
// 对rotation进行更新--转向去面对鼠标指针
Vector3 MousePosWorld = Camera.main.ScreenToWorldPoint(new Vector3(Input.mousePosition.x, Input.mousePosition.y, 0.0f));

MousePosWorld = new Vector3(MousePosWorld.x, 0.0f, MousePosWorld.z);

// 获取光标的方向
Vector3 LookDirection = MousePosWorld - ThisTransform.position;

// 对rotation进行固定更新
ThisTransform.localRotation = Quaternion.LookRotation(LookDirection.normalized, Vector3.up);
}
// 检查开火控制
if (Input.GetButtonDown(FireAxis) && CanFire)
{
foreach (Transform T in TurretTransforms)
AmmoManager.SpawnAmmo(T.position, T.rotation);

CanFire = false;
Invoke("EnableFire", ReloadDelay);
}
}

void EnableFire()
{
CanFire = true;
}

public void Die()
{
Destroy(gameObject);
}
}

计分功能——为文本对象编写脚本

GameController.cs

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
using UnityEngine;
using System.Collections;
using UnityEngine.UI;

public class GameController : MonoBehaviour
{
// 游戏的得分
public static int Score;

// 前缀
public string ScorePrefix = string.Empty;

// 分数文本对象
public Text ScoreText = null;

// 游戏结束文本
public Text GameOverText = null;

public static GameController ThisInstance = null;

void Awake()
{
ThisInstance = this;
}

void Update()
{
// 更新分数文本
if(ScoreText != null)
ScoreText.text = ScorePrefix + Score.ToString();
}

public static void GameOver()
{
if(ThisInstance.GameOverText != null)
ThisInstance.GameOverText.gameObject.SetActive(true);
}
}

ScoreOnDestroy.cs

1
2
3
4
5
6
7
8
9
10
11
12
using UnityEngine;
using System.Collections;

public class ScoreOnDestroy : MonoBehaviour
{
public int ScoreValue = 50;

void OnDestroy()
{
GameController.Score += ScoreValue;
}
}
0%