场景搭建
开始摆摆摆
摆个大概
Shit!为啥没有先加碰撞再CoCoCopy(手动微笑)
Window——Rendering——Lighting Setting 搞个氛围
到位!
GameManager
using System.Collections;
using UnityEngine;
using UnityEngine.SceneManagement;
using UnityEngine.UI;
namespace Complete
{
public class GameManager : MonoBehaviour
{
public int m_NumRoundsToWin = 5; // 赢得x回合即胜利
public float m_StartDelay = 3f; // 回合开始前等待x秒
public float m_EndDelay = 3f; // 回合结束后等待x秒
public CameraControl m_CameraControl; // 获取不同阶段的相机操控
public Text m_MessageText; // UI
public GameObject m_TankPrefab; // 坦克预制体
public TankManager[] m_Tanks; // 创建坦克玩家
private int m_RoundNumber; // 当前回合
private WaitForSeconds m_StartWait; // 回合开始前等待x秒
private WaitForSeconds m_EndWait; // 回合结束后等待x秒
private TankManager m_RoundWinner; // 当前回合胜者
private TankManager m_GameWinner; // 总回合胜者
private void Start()
{
m_StartWait = new WaitForSeconds (m_StartDelay);
m_EndWait = new WaitForSeconds (m_EndDelay);
//生成坦克
SpawnAllTanks();
//将坦克集合的变换数据传递给观察相机
SetCameraTargets();
//开始游戏
StartCoroutine (GameLoop ());
}
private void SpawnAllTanks()
{
for (int i = 0; i < m_Tanks.Length; i++)
{
m_Tanks[i].m_Instance =
Instantiate(m_TankPrefab, m_Tanks[i].m_SpawnPoint.position, m_Tanks[i].m_SpawnPoint.rotation) as GameObject;
m_Tanks[i].m_PlayerNumber = i + 1;
m_Tanks[i].Setup();
}
}
private void SetCameraTargets()
{
Transform[] targets = new Transform[m_Tanks.Length];
for (int i = 0; i < targets.Length; i++)
{
targets[i] = m_Tanks[i].m_Instance.transform;
}
m_CameraControl.m_Targets = targets;
}
private IEnumerator GameLoop ()
{
yield return StartCoroutine (RoundStarting ());
yield return StartCoroutine (RoundPlaying());
yield return StartCoroutine (RoundEnding());
if (m_GameWinner != null)
{
//游戏结束
SceneManager.LoadScene (0);
}
else
{
StartCoroutine (GameLoop ());
}
}
private IEnumerator RoundStarting ()
{
//重置坦克和观察相机
ResetAllTanks ();
DisableTankControl ();
m_CameraControl.SetStartPositionAndSize ();
//叠加回合数
m_RoundNumber++;
m_MessageText.text = "ROUND " + m_RoundNumber;
yield return m_StartWait;
}
private IEnumerator RoundPlaying ()
{
EnableTankControl ();
m_MessageText.text = string.Empty;
while (!OneTankLeft())
{
yield return null;
}
}
private IEnumerator RoundEnding ()
{
DisableTankControl ();
m_RoundWinner = null;
//获取回合胜者
m_RoundWinner = GetRoundWinner ();
//胜者加分
if (m_RoundWinner != null)
m_RoundWinner.m_Wins++;
//获取游戏胜者
m_GameWinner = GetGameWinner ();
//编辑游戏胜者告示
string message = EndMessage ();
m_MessageText.text = message;
yield return m_EndWait;
}
//是否决出胜负
private bool OneTankLeft()
{
//统计幸存者
int numTanksLeft = 0;
for (int i = 0; i < m_Tanks.Length; i++)
{
if (m_Tanks[i].m_Instance.activeSelf)
numTanksLeft++;
}
return numTanksLeft <= 1;
}
private TankManager GetRoundWinner()
{
for (int i = 0; i < m_Tanks.Length; i++)
{
if (m_Tanks[i].m_Instance.activeSelf)
return m_Tanks[i];
}
return null;
}
private TankManager GetGameWinner()
{
for (int i = 0; i < m_Tanks.Length; i++)
{
if (m_Tanks[i].m_Wins == m_NumRoundsToWin)
return m_Tanks[i];
}
return null;
}
private string EndMessage()
{
string message = "DRAW!";
if (m_RoundWinner != null)
message = m_RoundWinner.m_ColoredPlayerText + " WINS THE ROUND!";
message += "\n\n\n\n";
for (int i = 0; i < m_Tanks.Length; i++)
{
message += m_Tanks[i].m_ColoredPlayerText + ": " + m_Tanks[i].m_Wins + " WINS\n";
}
if (m_GameWinner != null)
message = m_GameWinner.m_ColoredPlayerText + " WINS THE GAME!";
return message;
}
private void ResetAllTanks()
{
for (int i = 0; i < m_Tanks.Length; i++)
{
m_Tanks[i].Reset();
}
}
private void EnableTankControl()
{
for (int i = 0; i < m_Tanks.Length; i++)
{
m_Tanks[i].EnableControl();
}
}
private void DisableTankControl()
{
for (int i = 0; i < m_Tanks.Length; i++)
{
m_Tanks[i].DisableControl();
}
}
}
}
CameraControl
using UnityEngine;
namespace Complete
{
public class CameraControl : MonoBehaviour
{
public float m_DampTime = 0.2f; // 每x秒调整对焦
public float m_ScreenEdgeBuffer = 4f; // 被观察对象与屏幕边缘预留空隙
public float m_MinSize = 6.5f; // 最小正交尺寸
[HideInInspector] public Transform[] m_Targets; // 被观察对象
private Camera m_Camera; // 观察相机
private float m_ZoomSpeed; // 伸缩速度
private Vector3 m_MoveVelocity; // 移动速度
private Vector3 m_DesiredPosition; // 目标位置
private void Awake ()
{
m_Camera = GetComponentInChildren<Camera> ();
}
private void FixedUpdate ()
{
Move ();
Zoom ();
}
private void Move ()
{
// 计算被观察对象平均位置
FindAveragePosition ();
// 平滑移动到目标位置
transform.position = Vector3.SmoothDamp(transform.position, m_DesiredPosition, ref m_MoveVelocity, m_DampTime);
}
private void FindAveragePosition ()
{
Vector3 averagePos = new Vector3 ();
int numTargets = 0;
// 被观察者位置叠加
for (int i = 0; i < m_Targets.Length; i++)
{
if (!m_Targets[i].gameObject.activeSelf)
continue;
averagePos += m_Targets[i].position;
numTargets++;
}
// 求平均位置
if (numTargets > 0)
averagePos /= numTargets;
averagePos.y = transform.position.y;
m_DesiredPosition = averagePos;
}
private void Zoom ()
{
// 当前大小平滑缩放到目标大小
float requiredSize = FindRequiredSize();
m_Camera.orthographicSize = Mathf.SmoothDamp (m_Camera.orthographicSize, requiredSize, ref m_ZoomSpeed, m_DampTime);
}
private float FindRequiredSize ()
{
// 发
Vector3 desiredLocalPos = transform.InverseTransformPoint(m_DesiredPosition);
// Start the camera's size calculation at zero.
float size = 0f;
for (int i = 0; i < m_Targets.Length; i++)
{
if (!m_Targets[i].gameObject.activeSelf)
continue;
Vector3 targetLocalPos = transform.InverseTransformPoint(m_Targets[i].position);
Vector3 desiredPosToTarget = targetLocalPos - desiredLocalPos;
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;
}
public void SetStartPositionAndSize ()
{
FindAveragePosition ();
transform.position = m_DesiredPosition;
m_Camera.orthographicSize = FindRequiredSize ();
}
}
}
敲黑板-知识点 transform.InverseTransformPoint
transform.localToWorldMatrix | 本地坐标转世界坐标的矩阵信息 |
transform.worldToLocalMatrix | 世界坐标转本地坐标的矩阵信息 |
transform.TransformDirection | 将向量从本地坐标转世界坐标,不受缩放影响 |
transform.InverseTransformDirection | 将向量从世界坐标转本地坐标,不受缩放影响 |
transform.TransformPoint | 将点从本地坐标转世界坐标,受缩放影响 |
transform.InverseTransformPoint | 将点从世界坐标转本地坐标,受缩放影响 |
transform.TransformVector | 本地坐标转世界坐标,受缩放影响,不受位置影响 |
transform.InverseTransformVector | 世界坐标转本地坐标,受缩放影响,不受位置影响 |
TankManager
using System;
using UnityEngine;
namespace Complete
{
[Serializable]
public class TankManager
{
public Color m_PlayerColor; // 皮肤颜色
public Transform m_SpawnPoint; // 出生点
[HideInInspector] public int m_PlayerNumber; // 玩家ID
[HideInInspector] public string m_ColoredPlayerText; // 颜色信息
[HideInInspector] public GameObject m_Instance; // 不解释
[HideInInspector] public int m_Wins; // 胜局统计
private TankMovement m_Movement; // 坦克操控系统
private TankShooting m_Shooting; // 坦克射击系统
private GameObject m_CanvasGameObject; // 血条UI
public void Setup ()
{
m_Movement = m_Instance.GetComponent<TankMovement> ();
m_Shooting = m_Instance.GetComponent<TankShooting> ();
m_CanvasGameObject = m_Instance.GetComponentInChildren<Canvas> ().gameObject;
// 统一ID信息
m_Movement.m_PlayerNumber = m_PlayerNumber;
m_Shooting.m_PlayerNumber = m_PlayerNumber;
// 编辑颜色信息
m_ColoredPlayerText = "<color=#" + ColorUtility.ToHtmlStringRGB(m_PlayerColor) + ">PLAYER " + m_PlayerNumber + "</color>";
// 坦克染色
MeshRenderer[] renderers = m_Instance.GetComponentsInChildren<MeshRenderer> ();
for (int i = 0; i < renderers.Length; i++)
{
renderers[i].material.color = m_PlayerColor;
}
}
public void DisableControl ()
{
m_Movement.enabled = false;
m_Shooting.enabled = false;
m_CanvasGameObject.SetActive (false);
}
public void EnableControl ()
{
m_Movement.enabled = true;
m_Shooting.enabled = true;
m_CanvasGameObject.SetActive (true);
}
public void Reset ()
{
m_Instance.transform.position = m_SpawnPoint.position;
m_Instance.transform.rotation = m_SpawnPoint.rotation;
m_Instance.SetActive (false);
m_Instance.SetActive (true);
}
}
}
敲黑板-知识点 [Serializable][HideInInspector]
当我们在Inspectorsan上操作数据时,Unity便将这些数据序列化为文件。游戏运行时,Unity会反序列化文件,获取数据赋给对象。继承MonoBehaviour的对象在instantiate()时,同样会触发序列化和反序列化。
[Serializable]:用于自定义的非抽象类/结构体,使其支持序列化;
[HideInInspector]:在Inspector面板中隐藏公有成员
有关Unity序列化内容请先参考https://www.cnblogs.com/devhyj/p/4342592.html,待我日后深究更博~
TankMovement
using UnityEngine;
namespace Complete
{
public class TankMovement : MonoBehaviour
{
public int m_PlayerNumber = 1; // ID
public float m_Speed = 12f; // 移动速度
public float m_TurnSpeed = 180f; // 旋转速度
public AudioSource m_MovementAudio; // 音效素材
public AudioClip m_EngineIdling; // Idle音效
public AudioClip m_EngineDriving; // Move音效
public float m_PitchRange = 0.2f; // 噪声变量
private string m_MovementAxisName; // 移动控制轴名称
private string m_TurnAxisName; // 旋转控制轴名称
private Rigidbody m_Rigidbody; // 受控刚体
private float m_MovementInputValue; // 接收移动值
private float m_TurnInputValue; // 接收旋转值
private float m_OriginalPitch; // 初始音调
private ParticleSystem[] m_particleSystems; // 粒子系统
private void Awake ()
{
m_Rigidbody = GetComponent<Rigidbody> ();
}
private void OnEnable ()
{
m_Rigidbody.isKinematic = false;
m_MovementInputValue = 0f;
m_TurnInputValue = 0f;
// 开启粒子系统
m_particleSystems = GetComponentsInChildren<ParticleSystem>();
for (int i = 0; i < m_particleSystems.Length; ++i)
{
m_particleSystems[i].Play();
}
}
private void OnDisable ()
{
m_Rigidbody.isKinematic = true;
// 关闭粒子系统
for(int i = 0; i < m_particleSystems.Length; ++i)
{
m_particleSystems[i].Stop();
}
}
private void Start ()
{
// 编辑控制轴名称
m_MovementAxisName = "Vertical" + m_PlayerNumber;
m_TurnAxisName = "Horizontal" + m_PlayerNumber;
// 存储初始音调
m_OriginalPitch = m_MovementAudio.pitch;
}
private void Update ()
{
// 读取控制轴数据
m_MovementInputValue = Input.GetAxis (m_MovementAxisName);
m_TurnInputValue = Input.GetAxis (m_TurnAxisName);
// 播放音效
EngineAudio ();
}
private void EngineAudio ()
{
if (Mathf.Abs (m_MovementInputValue) < 0.1f && Mathf.Abs (m_TurnInputValue) < 0.1f)
{
if (m_MovementAudio.clip == m_EngineDriving)
{
m_MovementAudio.clip = m_EngineIdling;
m_MovementAudio.pitch = Random.Range (m_OriginalPitch - m_PitchRange, m_OriginalPitch + m_PitchRange);
m_MovementAudio.Play ();
}
}
else
{
if (m_MovementAudio.clip == m_EngineIdling)
{
m_MovementAudio.clip = m_EngineDriving;
m_MovementAudio.pitch = Random.Range(m_OriginalPitch - m_PitchRange, m_OriginalPitch + m_PitchRange);
m_MovementAudio.Play();
}
}
}
private void FixedUpdate ()
{
Move ();
Turn ();
}
private void Move ()
{
// 计算位移
Vector3 movement = transform.forward * m_MovementInputValue * m_Speed * Time.deltaTime;
m_Rigidbody.MovePosition(m_Rigidbody.position + movement);
}
private void Turn ()
{
// 计算转角
float turn = m_TurnInputValue * m_TurnSpeed * Time.deltaTime;
Quaternion turnRotation = Quaternion.Euler (0f, turn, 0f);
m_Rigidbody.MoveRotation (m_Rigidbody.rotation * turnRotation);
}
}
}
TankShooting
using UnityEngine;
using UnityEngine.UI;
namespace Complete
{
public class TankShooting : MonoBehaviour
{
public int m_PlayerNumber = 1; // ID
public Rigidbody m_Shell; // 弹药
public Transform m_FireTransform; // 弹药生成点
public Slider m_AimSlider; // 瞄准UI
public AudioSource m_ShootingAudio; // 射击音效素材
public AudioClip m_ChargingClip; // 蓄力音效
public AudioClip m_FireClip; // 开火音效
public float m_MinLaunchForce = 15f; // 最小发射力度
public float m_MaxLaunchForce = 30f; // 最大发射力度
public float m_MaxChargeTime = 0.75f; // 最长蓄力时间
private string m_FireButton; // 开火键
private float m_CurrentLaunchForce; // 发射力度
private float m_ChargeSpeed; // 蓄力速度
private bool m_Fired; // 开火状态
private void OnEnable()
{
m_CurrentLaunchForce = m_MinLaunchForce;
m_AimSlider.value = m_MinLaunchForce;
}
private void Start ()
{
// 编辑开火键名称
m_FireButton = "Fire" + m_PlayerNumber;
// 计算蓄力速度
m_ChargeSpeed = (m_MaxLaunchForce - m_MinLaunchForce) / m_MaxChargeTime;
}
private void Update ()
{
// 蓄力状态可视化
m_AimSlider.value = m_MinLaunchForce;
// 蓄满力自动开火
if (m_CurrentLaunchForce >= m_MaxLaunchForce && !m_Fired)
{
m_CurrentLaunchForce = m_MaxLaunchForce;
Fire ();
}
// 按下开火键
else if (Input.GetButtonDown (m_FireButton))
{
m_Fired = false;
m_CurrentLaunchForce = m_MinLaunchForce;
m_ShootingAudio.clip = m_ChargingClip;
m_ShootingAudio.Play ();
}
// 蓄力中
else if (Input.GetButton (m_FireButton) && !m_Fired)
{
m_CurrentLaunchForce += m_ChargeSpeed * Time.deltaTime;
m_AimSlider.value = m_CurrentLaunchForce;
}
// 松开开火
else if (Input.GetButtonUp (m_FireButton) && !m_Fired)
{
Fire ();
}
}
private void Fire ()
{
m_Fired = true;
// 生成弹药
Rigidbody shellInstance =
Instantiate (m_Shell, m_FireTransform.position, m_FireTransform.rotation) as Rigidbody;
// 发射弹药
shellInstance.velocity = m_CurrentLaunchForce * m_FireTransform.forward;
// 射击音效
m_ShootingAudio.clip = m_FireClip;
m_ShootingAudio.Play ();
// 重置蓄力状态
m_CurrentLaunchForce = m_MinLaunchForce;
}
}
}
TankHealth
using UnityEngine;
using UnityEngine.UI;
namespace Complete
{
public class TankHealth : MonoBehaviour
{
public float m_StartingHealth = 100f; // 初始血量
public Slider m_Slider; // 血条UI
public Image m_FillImage; // 血条样式
public Color m_FullHealthColor = Color.green; // 满血颜色
public Color m_ZeroHealthColor = Color.red; // 缺血颜色
public GameObject m_ExplosionPrefab; // 爆炸特效
private AudioSource m_ExplosionAudio; // 爆炸音效
private ParticleSystem m_ExplosionParticles; // 爆炸特效
private float m_CurrentHealth; // 当前血量
private bool m_Dead; // 是否死亡
private void Awake ()
{
m_ExplosionParticles = Instantiate (m_ExplosionPrefab).GetComponent<ParticleSystem> ();
m_ExplosionAudio = m_ExplosionParticles.GetComponent<AudioSource> ();
m_ExplosionParticles.gameObject.SetActive (false);
}
private void OnEnable()
{
//信息重置
m_CurrentHealth = m_StartingHealth;
m_Dead = false;
SetHealthUI();
}
public void TakeDamage (float amount)
{
// 掉血
m_CurrentHealth -= amount;
SetHealthUI ();
// 判断血槽已空
if (m_CurrentHealth <= 0f && !m_Dead)
{
OnDeath ();
}
}
private void SetHealthUI ()
{
// 刷新血条UI
m_Slider.value = m_CurrentHealth;
// 根据血量插值求出血条颜色
m_FillImage.color = Color.Lerp (m_ZeroHealthColor, m_FullHealthColor, m_CurrentHealth / m_StartingHealth);
}
private void OnDeath ()
{
m_Dead = true;
// 死亡特效
m_ExplosionParticles.transform.position = transform.position;
m_ExplosionParticles.gameObject.SetActive (true);
m_ExplosionParticles.Play ();
m_ExplosionAudio.Play();
gameObject.SetActive (false);
}
}
}
ShellExplosion
using UnityEngine;
public class ShellExplosion : MonoBehaviour
{
public LayerMask m_TankMask; // 坦克掩码
public ParticleSystem m_ExplosionParticles; // 爆炸特效
public AudioSource m_ExplosionAudio; // 爆炸音效
public float m_MaxDamage = 100f; // 最大伤害
public float m_ExplosionForce = 1000f; // 爆炸力
public float m_MaxLifeTime = 2f; // 最大持续时间
public float m_ExplosionRadius = 5f; // 伤害半径
private void Start()
{
// 一段时间后销毁自身
Destroy(gameObject, m_MaxLifeTime);
}
private void OnTriggerEnter(Collider other)
{
// 伤害半径内所有受击坦克
}
private float CalculateDamage(Vector3 targetPosition)
{
// 根据距离计算伤害
return 0f;
}
}
UIDirectionControl
using UnityEngine;
public class UIDirectionControl : MonoBehaviour
{
public bool m_UseRelativeRotation = true;
private Quaternion m_RelativeRotation;
private void Start()
{
m_RelativeRotation = transform.parent.localRotation;
}
private void Update()
{
if (m_UseRelativeRotation)
transform.rotation = m_RelativeRotation;
}
}
完整项目:Asset Store —— Tanks! Tutorial