從零開始用Unity做一個海戰(zhàn)游戲(中)

作者:沈琰
本篇難度:★★☆☆☆
前言
好長時間沒更新了,但是請放心,我并沒有細(xì)軟跑。

先看看這段時間都加了些啥新東西:

給還沒看過上期的同學(xué)做個簡短的上期回顧:從零開始用Unity做一個海戰(zhàn)游戲(上)
上期里我們親手捏出了一條小船并裝上了第一種武器火炮,然后實(shí)現(xiàn)了炮塔旋轉(zhuǎn)的邏輯。這期主要的內(nèi)容將是船的移動邏輯和武器種類的擴(kuò)展。
那么不多說廢話了,我們繼續(xù)。
船的移動
本來一開始我覺得移動邏輯挺容易寫的,但是嘗試了一下發(fā)現(xiàn)并不是很簡單。

現(xiàn)在只有一條船,但是當(dāng)有多條船一起移動時會涉及到碰撞問題,所以最初的想法是用物理系統(tǒng)來實(shí)現(xiàn)這個移動邏輯。
實(shí)際上用Rigidbody.AddForce()的方法來實(shí)現(xiàn)的話,一是用物理材質(zhì)模擬阻力之類的參數(shù)不太好調(diào)整,二是受力點(diǎn)的位置也不太好選取,容易出一些奇奇怪怪的BUG。所以就直接通過修改Rigidbody.velocity的值來實(shí)現(xiàn)移動。同時為避免在碰撞的時候船的朝向出問題,鎖了Y軸的移動和X,Z軸的旋轉(zhuǎn)。
那么現(xiàn)在需求就轉(zhuǎn)化為獲取一個Vector3類型的速度值了,然后再來思考一下具體的移動方式。
實(shí)際中的船只移動邏輯極為復(fù)雜,我們當(dāng)然沒有必要去做到那么擬真,就以類似游戲中的船移動模式作為參考:
船的移動方式跟地面上的載具不太一樣。首先船一般都是有較大的慣性的,表現(xiàn)在實(shí)際的移動中就是船的加速和減速都比較困難,需要較長時間船才能達(dá)到最大速度或者完全停止。
這個還算比較容易實(shí)現(xiàn),簡單來說就是先得到船的移動方向的向量后,把加速度去做差值處理,每一幀去更新這個值。
而船的轉(zhuǎn)向就比較麻煩一些了。因?yàn)榇菦]有辦法在靜止的狀態(tài)原地打轉(zhuǎn)的,因此船的轉(zhuǎn)向速度的具體值是跟朝向方向的速度掛鉤的,但兩者又不是簡單的線性遞增關(guān)系,否則速度較快時移動會極為鬼畜。
本來想自定義一條曲線去調(diào)整這個線性關(guān)系的,想了想最終還是用了個簡單粗暴的方法,定義一個轉(zhuǎn)速最大時所達(dá)到的速度值,然后以船的當(dāng)前速度和它的比率得到線性關(guān)系。同時自定義一條曲線來模擬水面阻力:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
?
public class Move : MonoBehaviour
{
??? //阻力
??? float Resistance
??? {
??????? get
??????? {
??????????? return ResistanceCurve.Evaluate(CurVeloticy.magnitude);
??????? }
??? }
?
?
??? public float enginePower = 100;
?
??? float speedAmount = 0f;
?
??? float acceleration
??? {
??????? get
??????? {
??????????? return Mathf.Max(0.1f, enginePower - (ShipMass + Resistance));
??????? }
??? }
?
?
??? float RotateSpeed = 720;
?
?
??? //EngineState engineState;
?
??? Rigidbody rig;
?
??? public AnimationCurve ResistanceCurve = new AnimationCurve();
?
??? //base
??? public float ShipMass = 80f;
?
??? public float ShipLength = 20f;
?
?
?
?
??? //move
??? Vector3 PrevPosition;
?
??? Vector3 CurVeloticy;
?
??? Vector3 TargetVeloticy;
?
??? Vector3 ActualVeloticy;
?
?
?
??? Vector3 ForwardAmount;
?
??? float TurnAmount;
?
??? float ActualTurnAmount;
?
???? public float maxRotateVelotocy=2;
?
?
?
?
??? //control
??? public bool speedUp = false;
?
??? public bool turnLeft = false;
?
??? public bool turnRight = false;
?
?
?
??? void Start()
??? {
?
??????? rig = GetComponent<Rigidbody>();
???????
??????? PrevPosition = transform.position;
??????? ActualVeloticy = Vector3.zero;
??????? ForwardAmount = transform.forward;
??????? transform.position = new Vector3(transform.position.x, 0, transform.position.y);
??? }
?
??? private void FixedUpdate()
??? {
??????? InputProcess();
??????? InterpolationVeloticy();
??????? MovementUpdate();
??? }
?
??
?
??? void InterpolationVeloticy()
??? {
??????? ActualVeloticy = Vector3.Lerp(ActualVeloticy, TargetVeloticy, 0.005f);
?
??????? ActualTurnAmount = Mathf.Lerp(ActualTurnAmount, TurnAmount, 0.1f);
??? }
?
?
?
??? void MovementUpdate()
??? {
??????? rig.velocity = Quaternion.AngleAxis(ActualTurnAmount, transform.up) * ActualVeloticy;
?
?
??????? if (rig.velocity.normalized != Vector3.zero)
??????? {
??????????? transform.forward = rig.velocity.normalized;
??????? }
?
??????? CurVeloticy = transform.position - PrevPosition;
??????? PrevPosition = transform.position;
??? }
?
?
??? void InputProcess()
??? {
??????? if (speedUp)
??????? {
??????????? TargetVeloticy = ForwardAmount * (speedAmount + acceleration);
??????? }
??????? else
??????? {
??????????? TargetVeloticy = ForwardAmount * (Mathf.Max(0, speedAmount - Resistance));
??????? }
?
??????? float rotateRate = Mathf.Clamp(CurVeloticy.magnitude / maxRotateVelotocy, 0f, 1f);
?????
??????? float deg = RotateSpeed *rotateRate;
???????
??????? if (turnLeft)
????? ??{????
??????????? TurnAmount -= deg * Time.deltaTime;
??????? }
??????? if (turnRight)
??????? {
??????????? TurnAmount += deg * Time.deltaTime;
??????? }??
??? }
?
}
?
調(diào)整一下參數(shù)到一個合適的手感,效果如下:

因?yàn)闀簳r只是在平面上移動,沒有水面效果的參照,先用粒子系統(tǒng)或者拖尾渲染器做一個簡單的尾跡,然后讓相機(jī)跟隨船移動:
public GameObject Target;
?public float smoothing = 3;
?
?? private void LateUpdate()
?? {
???????
?????? Vector3 tracePos = new Vector3(Target.transform.position.x, 100, Target.transform.position.z);
??????? transform.position = Vector3.Lerp(transform.position, tracePos, smoothing * Time.deltaTime);
?
?? }
?

簡易的AI
如果看了之前移動的代碼,可以發(fā)現(xiàn)我把移動的輸入部分抽象成了3個布爾值,分別是加速,左轉(zhuǎn)和右轉(zhuǎn),其目的是方便寫一個簡單的AI去控制船,同時玩家控制部分也可以通過修改這三個布爾值實(shí)現(xiàn)。
這樣一來AI的移動邏輯寫起來就很簡單了:

同理武器和炮塔腳本也稍微修改一下, 只傳入一個目標(biāo)對象作為瞄準(zhǔn)的目標(biāo),用物理系統(tǒng)的球形檢測當(dāng)做“雷達(dá)”來尋找目標(biāo)。
AI部分代碼:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
?
[DefaultExecutionOrder(1000)]
public class AIController : MonoBehaviour
{
??? Move movement;
?
??? weapons[] weapons;
?
??? TurretsContorl[] Turrets;
?
??? //尋敵范圍
??? public float radiusForSearchTarget;
?
??? //目標(biāo)距離的余量
??? public float stopDistence;
?
??? //目標(biāo)夾角的余量
??? public float stopDegrees;
?
??? //目標(biāo)
??? GameObject target = null;
?
?
??? void Start ()
??? {
??????? movement = transform.GetComponent<Move>();
?
??????? weapons = transform.GetComponentsInChildren<weapons>(true);
?
??????? Turrets = transform.GetComponentsInChildren<TurretsContorl>(true);??????
??? }
??????????????????
???????? void Update ()
??? {
? ??????SearchEnemy();
???????? }
?
??? void SearchEnemy()
??? {
??????? Collider[] col = Physics.OverlapSphere(transform.position, radiusForSearchTarget, 1 << LayerMask.NameToLayer("Player"));
?
??????? //玩家只有一個,先不考慮多對多的戰(zhàn)斗
??????? if(col.Length>0)
??????? target = col[0].gameObject ?? null;
?
??????? Vector3 targetPos;
?
??????? if (target != null)
??????? {
?
??????????? targetPos = new Vector3(target.transform.position.x, transform.position.y, target.transform.position.z);
??????? }
??????? else
??????? {
?????????? ?targetPos = Vector3.zero;
??????? }
??????? AIControl(target != null, targetPos);
??? }
?
?
??? void MoveToTarget(Vector3 targetPos_fixed)
??? {
???????
??????? float dis = Vector3.Distance(targetPos_fixed, transform.position);
?
??????? if(dis>stopDistence)
??????? {
??????????? movement.speedUp = true;
??????? }
??????? else
??????? {
??????????? movement.speedUp = false;
??????? }
??? }
?
??? void AimAtTarget(Vector3 targetPos_fixed)
??? {
??????? for(int i=0;i<Turrets.Length;i++)
??????? {
??????????? Turrets[i].targetPos = targetPos_fixed;
??????????? Turrets[i].Fire();
??????? }
??? }
?
?
?
??? void RotateToTarget(Vector3 targetPos_fixed)
??? {?
??????? Vector3 tarDir =(targetPos_fixed - transform.position).normalized;
?
??????? bool tarIsRight = Vector3.Cross(transform.forward, tarDir).y > 0;
?
??????? float angle = Vector3.Angle(transform.forward, tarDir);
??????? if(angle>stopDegrees)
??????? {????
??????????? if (tarIsRight)
??????????? {
??????????????? movement.turnRight = true;
???????????? ???movement.turnLeft = false;
??????????? }
??????????? else
??????????? {
??????????????? movement.turnRight = false;
??????????????? movement.turnLeft = true;
??????????? }
??????? }
??????? else
??????? {
??????????? movement.turnRight = false;
??????? ????movement.turnRight = false;
??????? }
??? }
?
??? void AIControl(bool hasTarget,Vector3 targetPos_fixed)
??? {
??????? if(!hasTarget)
??????? {
??????????? movement.speedUp = false;
??????????? movement.turnRight = false;
??????????? movement.turnLeft = false;
??????????? for(int i=0;i<Turrets.Length;i++)
??????????? {
??????????????? Turrets[i].targetPos = targetPos_fixed;
??????????? }
??????????? return;
??????? }
??????? MoveToTarget(targetPos_fixed);
??????? RotateToTarget(targetPos_fixed);
??????? AimAtTarget(targetPos_fixed);
???????
??? }
??? //尋敵范圍可視
??? private void OnDrawGizmos()
??? {
??????? Gizmos.color = Color.red;
??????? Gizmos.DrawWireSphere(transform.position, radiusForSearchTarget);
??? }
}
?
沒有目標(biāo)時就先讓它呆呆的什么都別做,先勾引一下AI測試反應(yīng):

勉強(qiáng)還能用,AI暫時就先這么湊合著,等后面游戲玩法結(jié)構(gòu)搭起來了再考慮擴(kuò)展或者修改。
武器系統(tǒng)擴(kuò)展
原作多樣的武器系統(tǒng)是玩法的核心,所以我們自然也不能只有簡簡單單的一門炮,得讓武器系統(tǒng)豐富一些。
初步的想法是先添加上海戰(zhàn)里常用的兩種武器:導(dǎo)彈和魚雷。
但在寫的時候我突然意識到一個問題,目前的三種武器系統(tǒng)的發(fā)射方式和發(fā)射后的移動邏輯均不相同,除了炮塔旋轉(zhuǎn)部分的腳本能夠復(fù)用,這意味著算上子彈和發(fā)射器我總共要寫6個腳本,但是其中很大一部分代碼都是相同的。這樣算起來不僅工作量大,而且后續(xù)也不好擴(kuò)展。
作為一個懶癌晚期的患者我自然是很煩相同的代碼要寫幾遍這種情況,所以是時候祭出面向?qū)ο蟮姆椒恕?/p>
首先把三者共同的部分抽象成發(fā)射器和投射物的基類,不同的部分如移動方式等用虛方法寫出來并在子類里重載,以火炮繼承發(fā)射器為例子:
發(fā)射器基類:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
?
public class Emitter : MonoBehaviour
{
??? [SerializeField]
??? protected float FireFrequency = 0.4f;
?
??? protected float PrevFireTime = float.MinValue;
?
?
??? [SerializeField]
??? protected ParticleSystem[] Particles = new ParticleSystem[0];
?
??? [SerializeField]
??? protected Transform muzzle;
?
??? protected bool controllerIsPlayer = false;
?
??? [SerializeField]
??? protected TurretsContorl turrets;
?
??? [SerializeField]
??? protected GameObject projectile;
?
?
??? public float speed;
?
??? public float lifeTime;
?
??? public int damage;
?
??? public bool ReadyToShoot
??? {
??????? get
??????? {
??????????? if (controllerIsPlayer)
??????????? {
??????????????? return PrevFireTime + FireFrequency < Time.time;
???? ???????}
??????????? else
??????????? {
??????????????? return (PrevFireTime + FireFrequency < Time.time) && turrets.inShootArea;
??????????? }
??????? }
?
??? }
??? protected virtual void Start()
??? {
??????? Transform root = GetRoot(transform);
?
??????? if (root.tag == "Player")
??????? {
??????????? controllerIsPlayer = true;
??????? }
??????? if (!turrets)
??????? {
??????????? turrets = transform.GetComponentInParent<TurretsContorl>();
??????? }
??????? if (!muzzle)
??????? {
??????????? muzzle = transform.Find("Muzzle").transform;
??????? }
?
??? }
??? protected virtual void Update()
??? {
?
??? }
?
??? Transform GetRoot(Transform t)
??? {
??????? if (t.parent == null)
??????? {
??????????? return t;
??????? }
??????? else
??????? {
??????????? return GetRoot(t.parent);
??????? }
??? }
??? //基類的發(fā)射方法 ,先空著,在子類重載
??? protected virtual void Shoot()
??? {
??????
?
??? }
?
??? public virtual void Fire()
??? {
??????? if (ReadyToShoot)
??????? {
???????????
??????????? Shoot();
??????????? PlayAllParticles();
??????????? PrevFireTime = Time.time;
??????? }
??? }
??? void PlayAllParticles()
??? {
??????? for (int i = 0; i < Particles.Length; i++)
??????? {
??????????? Particles[i].Play();
??????? }
??? }
#if UNITY_EDITOR
?
??? protected virtual void Reset()
??? {
??????? ParticleSystem[] p = GetComponentsInChildren<ParticleSystem>(true);
??????? Particles = p;
?
??????? muzzle = transform.Find("Muzzle").transform;
??????? turrets = transform.GetComponentInParent<TurretsContorl>();
??? }
#endif
}
?
還記得上一篇文章里我們用動畫曲線實(shí)現(xiàn)了一個開炮退膛的小動畫,基類的代碼里并沒有寫,因?yàn)槟鞘侵挥谢鹋诓庞械奶厥夥椒?但是我們可以在子類的代碼里實(shí)現(xiàn)。
重寫后的子類火炮的代碼:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
?
public class Artillery : Emitter
{
??? [SerializeField]
??? private Transform model;
??? public Transform Model
??? {
??????? get
??????? {
??????????? return model ? model : transform.Find("Model").transform;
??????? }
??? }
?
??? [SerializeField]
??? private AnimationCurve LerpCurve = AnimationCurve.EaseInOut(0f, -0.4f, 0.4f, 0f);
?
??? protected override void Update()
??? {
??????? AnimUpdate();
??? }
?
??? void AnimUpdate()
??? {
??????? float t = Time.time - PrevFireTime;
??????? model.localPosition = Vector3.forward * LerpCurve.Evaluate(t);
??? }
?
??? protected override void Shoot()
??? {
???????
??????? Instantiate(projectile).GetComponent<Projectile>().Init(muzzle.position, muzzle.forward, speed, lifeTime, damage, gameObject.layer);
??? }
?
#if UNITY_EDITOR
??? protected override void Reset()
??? {
??????? base.Reset();
??????? model = transform.Find("Model").transform;
??? }
#endif
}
?

將子類腳本掛載到火炮上依然能正常的發(fā)射子彈了,我們再來實(shí)現(xiàn)其他兩種武器系統(tǒng)。


我的想法是導(dǎo)彈發(fā)射器與火炮的發(fā)射方式不同在間隔和鎖定目標(biāo),導(dǎo)彈一口氣可以連著發(fā)射多發(fā),然后會有一個較長的重裝填時間,并且發(fā)射導(dǎo)彈時還會傳入一個目標(biāo)的坐標(biāo)。同時導(dǎo)彈的速度會隨著時間加速并追蹤目標(biāo)。
基于長度原因剩下的代碼就不貼出來了,基本想法與上面并無太大區(qū)別,有興趣的同學(xué)可以下載工程自行研究。直接看看完成以后的效果:


魚雷我則是設(shè)定的同時發(fā)射多發(fā),然后在給定的角度下以一個均勻的扇形彈道發(fā)射,并且速度是隨時間遞減的。


結(jié)束
看了下至今為止的成果,可以說已經(jīng)頗具雛形了,成就感滿滿啊。

下一期文章的內(nèi)容主要就是不同武器間攻擊判定的邏輯,到時候我們的小船將能真正的戰(zhàn)斗起來。
不出意外下一次更新應(yīng)該就在十一之后了,這里先預(yù)祝大家國慶快樂。
限于水準(zhǔn)文章肯定會有疏漏和不足,還希望大家能在評論中指正。
感謝觀看到此,下期再見。(放心,絕對不會太監(jiān))

本期工程地址:https://github.com/tank1018702/unity-004
想系統(tǒng)學(xué)習(xí)游戲開發(fā)的童鞋,歡迎訪問?http://levelpp.com/? ? ? ????
游戲開發(fā)攪基QQ群:869551769? ? ? ? ???
微信公眾號:皮皮關(guān)