TANKS! Unity Tutorial 项目学习记录

  1. 1. API
    1. 1.1. SmoothDamp
    2. 1.2. InverseTransformPoint
    3. 1.3. Physics
      1. 1.3.1. Physics.OverlapSphere
    4. 1.4. Rigidbody
      1. 1.4.1. targetRigidbody.AddExplosionForce
  2. 2. Manual
    1. 2.1. 相机的两种投影方式
    2. 2.2. Aodio Mixer
  3. 3. 问题与解决
    1. 3.1. 移动和旋转问题
  4. 4. 代码记录
    1. 4.1. 相机的平滑运动
    2. 4.2. 炮弹的爆炸和伤害判定
  5. 5. 子弹的对象池模式
  6. 6. 学习总结
    1. 6.1. 游戏循环模式(协程完成)
    2. 6.2. 游戏管理模式
    3. 6.3. 可能的心得(……..)

API

SmoothDamp

平滑的改变当前值至另一个值

1
2
3
Mathf.SmoothDamp(float current, float target, ref float currentVelocity, float smoothTime, float maxSpeed = Mathf.Infinity, float deltaTime = Time.deltaTime);

Vector3 SmoothDamp(Vector3 current, Vector3 target, ref Vector3 currentVelocity, float smoothTime, float maxSpeed = Mathf.Infinity, float deltaTime = Time.deltaTime);

*current*:当前位置 *target*:尝试达到的目标值 *currentVelocity*:当前速度,该值在每次调用时都会由函数修改。 *smoothTime*:达到目标值的时间 *maxSpeed*:最大速度 *deltaTime*:默认为Time.deltatime *ref关键字*:相当于c的指针传参,及引用传参。

InverseTransformPoint

1
public Vector3 InverseTransformPoint(Vector3 position);

将position这个Vector3类型变量转化为 以V3的世界坐标为零点基准的情况下 position相对于V3的坐标值。

Physics

Physics.OverlapSphere

检测范围内的Collider
public static Collider[] OverlapSphere(Vector3 position, float radius, int layerMask = AllLayers, QueryTriggerInteraction queryTriggerInteraction = QueryTriggerInteraction.UseGlobal);

*position*:圆心 *radius*:检测半径 *layerMask*:检测层级 *queryTriggerInteraction*:判断是否应该检测Trigger

Rigidbody

targetRigidbody.AddExplosionForce

爆炸力将随着到物体的距离变小。
public void AddExplosionForce(float explosionForce, Vector3 explosionPosition, float explosionRadius, float upwardsModifier = 0.0f, ForceMode mode = ForceMode.Force));

*explosionForce*:爆炸的力量(会根据距离变化) *explosionPosition*:爆炸中心 *explosionRadius*:爆炸半径 *upwardsModifier*:可以调整爆炸的位置,让物体有被炸起来向上的效果,但爆炸本身的位置不变 *ForceMode*:对物体施加力的方法

Manual

相机的两种投影方式

相机的两种投影方式(prejection)

透视投影(Perspective)(左): 正交投影的观察体是长方体,它使用一组平行投影将三维对象投影到投影平面上去,即场景中的物体没有近大远小的效果。

正交投影(Orthographic)(右): 透视投影的观察体是视锥体,它使用一组由投影中心产生的放射投影线,将三维对象投影到投影平面上去,即屏幕中的物体存在透视效果

Aodio Mixer

类似于Windows的音量合成器,但更为复杂
可以用来进行多种音效的混合表现
要用可百度学习

问题与解决

移动和旋转问题

有问题的代码

1
2
Vector3 move = Vector3.forward * m_MovementInputValue * Time.deltaTime * m_Speed;
m_Rigidbody.MovePosition(m_Rigidbody.position + move);

此代码会导致物体旋转后会继续以世界坐标的z轴为前后方向,而导致旋转看起来不起作用,像坐标轴没有跟着旋转一样


正确的代码

1
2
Vector3 move = transform.forward * m_MovementInputValue * Time.deltaTime * m_Speed;
m_Rigidbody.MovePosition(m_Rigidbody.position + move);

修改后一切正常


Vector3.forward和transform.forward的区别

Vector3.forward的值永远是世界坐标(0,0,1),
而transform.forward是世界坐标对应的物体坐标的轴的向量

代码记录

相机的平滑运动

移动相关
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

private void Move()
{
FindAveragePosition();

transform.position = Vector3.SmoothDamp(transform.position, m_DesiredPosition, ref m_MoveVelocity, m_DampTime);
}


/// <summary>
/// 找到多个Player的中间位置
/// </summary>
private void FindAveragePosition()
{
Vector3 averagePos = new Vector3();

//记录还存在的Player的数量
int numTargets = 0;

for (int i = 0; i < m_Targets.Length; i++)
{
if (!m_Targets[i].gameObject.activeSelf)
continue;

// 记录Player的位置和
averagePos += m_Targets[i].position;
numTargets++;
}

//得到中间位置
if (numTargets > 0)
averagePos /= numTargets;

//保证相机的y轴不移动
averagePos.y = transform.position.y;

m_DesiredPosition = averagePos;
}

缩放相关
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

private void Zoom()
{
float requiredSize = FindRequiredSize();
m_Camera.orthographicSize = Mathf.SmoothDamp(m_Camera.orthographicSize, requiredSize, ref m_ZoomSpeed, m_DampTime);
}


/// <summary>
/// 找到需要的相机的最大大小
/// </summary>
/// <returns>返回相机的合适大小</returns>
private float FindRequiredSize()
{
//获得相机将要到达的位置的相对坐标
Vector3 desiredLocalPos = transform.InverseTransformPoint(m_DesiredPosition);

float size = 0f;

//获得还存活的Player的所需要的屏幕最大值
for (int i = 0; i < m_Targets.Length; i++)
{
if (!m_Targets[i].gameObject.activeSelf)
continue;

//转化Player的坐标为相对值
Vector3 targetLocalPos = transform.InverseTransformPoint(m_Targets[i].position);

//获得Player中心位置与Player位置的差值
Vector3 desiredPosToTarget = targetLocalPos - desiredLocalPos;

//计算x/y轴的相对大小,并取最大值
size = Mathf.Max (size, Mathf.Abs (desiredPosToTarget.y));

size = Mathf.Max (size, Mathf.Abs (desiredPosToTarget.x) / m_Camera.aspect);
}

//添加缓冲区
size += m_ScreenEdgeBuffer;

//防止屏幕缩小
size = Mathf.Max(size, m_MinSize);

return size;
}

炮弹的爆炸和伤害判定

爆炸相关
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
private void OnTriggerEnter(Collider other)
{

//获得爆炸范围内的坦克collider
Collider[] colliders = Physics.OverlapSphere(transform.position, m_ExplosionRadius, m_TankMask);//获得爆炸范围内的坦克collider

for (int i = 0; i < colliders.Length; i++)
{
Rigidbody targetRigidbody = colliders[i].GetComponent<Rigidbody>();
if (!targetRigidbody)
continue;

//对范围内的tank的刚体施加一个力
targetRigidbody.AddExplosionForce(m_ExplosionForce, transform.position, m_ExplosionRadius);//将

//获得Tank的血量属性
TankHealth tankHealth = targetRigidbody.GetComponent<TankHealth>();

if (!tankHealth)
continue;

//计算伤害
float damage = CalculateDamage(targetRigidbody.position);

//造成伤害
tankHealth.TakeDamage(damage);
}

//粒子效果和声音的播放
m_ExplosionParticles.transform.parent = null;
m_ExplosionParticles.Play();
m_ExplosionAudio.Play();

//销毁
Destroy(m_ExplosionParticles.gameObject, m_ExplosionParticles.main.duration);
Destroy(gameObject);
}

计算伤害
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
private float CalculateDamage(Vector3 targetPosition)
{
//计算Tank和炸弹的向量
Vector3 explosionToTarget = targetPosition - transform.position;

//获得距离
float explosionDistance = explosionToTarget.magnitude;

//计算相对距离比例
float relativeDistance = (m_ExplosionRadius - explosionDistance) / m_ExplosionRadius;

//计算伤害
float damage = relativeDistance * m_MaxDamage;

//排除Tank在边缘时 相对比例 为负数的情况
damage = Mathf.Max(0, damage);

return damage;
}

子弹的对象池模式

子弹
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

public class ShellExplosion : MonoBehaviour,IPooler
{

/*
...一些其他代码
*/

//生成时的初始化
public void OnSpawning()
{
/* 初始化相关 */
StartCoroutine(Spawning());
}


//调用协程,一定时间后重置
IEnumerator Spawning()
{
yield return m_TimeToFalse;
m_ExplosionParticles.transform.parent = gameObject.transform;
m_ExplosionParticles.transform.position = gameObject.transform.position;
gameObject.transform.position = new Vector3(0, 0, 0);
gameObject.SetActive(false);
}
}

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

public class ShellPool : MonoBehaviour
{
/// <summary>
/// 对象池
/// tag:名称
/// prefab;预制体
/// size:对象池的大小
/// </summary>
[System.Serializable]
public class Pool
{
public string tag;
public GameObject prefab;
public int size;
}

//对象池的父物体
public Transform parentTransform;

public List<Pool> poolList;
public Dictionary<string, Queue<GameObject>> poolDictionary;

//单例模式
public static ShellPool shellPoolInsatance;
private void Awake()
{
if (shellPoolInsatance == null)
shellPoolInsatance = this;
else if (shellPoolInsatance != this)
Destroy(this);
}

/// <summary>
/// 初始化生成所有对象池的物体,并添加到对象池字典中
/// </summary>
void Start()
{
poolDictionary = new Dictionary<string, Queue<GameObject>>();

foreach (var pool in poolList)
{
Queue<GameObject> tPool = new Queue<GameObject>();

for (int i = 0; i < pool.size; i++)
{
GameObject tShell = Instantiate(pool.prefab, parentTransform,true);
tShell.SetActive(false);
tPool.Enqueue(tShell);
}

poolDictionary.Add(pool.tag, tPool);
}
}

/// <summary>
/// 从对象池中生成(获得)物体
/// </summary>
/// <param name="tag">物体名称</param>
/// <param name="position">生成位置</param>
/// <param name="rotation">生成旋转</param>
/// <returns>返回生成的对象</returns>
public GameObject SpawnFromPool(string tag, Vector3 position, Quaternion rotation)
{
//判断是否存在需要生成的物体
if(!poolDictionary.ContainsKey(tag))
{
Debug.Log(tag + "不存在");
return null;
}

GameObject theSpawnObj = poolDictionary[tag].Dequeue();
Debug.Log(theSpawnObj);

theSpawnObj.SetActive(true);
theSpawnObj.transform.position = position;
theSpawnObj.transform.rotation = rotation;

//获得IPooler接口,并调用初始化函数
IPooler poolSpawn = theSpawnObj.GetComponent<IPooler>();
if (poolSpawn != null)
poolSpawn.OnSpawning();

poolDictionary[tag].Enqueue(theSpawnObj);

return theSpawnObj;
}
}


IPooler
1
2
3
4
5
6
7
//接口,方便生成物体后的初始化函数调用

interface IPooler
{
public void OnSpawning();
}

学习总结

游戏循环模式(协程完成)

循环模式
Tanks的流程控制

游戏管理模式

一些游戏物体的代码不需要继承MonoBehaviour(无需挂载),只当实例化后赋予其GameObject或直接更具里面的信息实例化一个物体。例如此例中的Tank 或者 一些随机地图的部分地图信息

可能的心得(……..)

  • 协程内调用多个协程,只会在上一个协程调用完成后,下一个协程才会开始
  • 回合制的游戏可以使用协程控制游戏流程,开始、游玩、结束,都很清晰明了
  • 写代码时因该将所有功能块写成函数,可以让代码结构更清晰
  • 尽量将可能的变量全定义在类的开头,理由同上
//