事業(yè)單位網(wǎng)站開發(fā)工作規(guī)程網(wǎng)站優(yōu)化seo方案
上期將基礎的移動系統(tǒng)搭建完畢后就可以開始搭建更加復雜的系統(tǒng)部分了
前排提示,由于一開始僅思考如何完成操作相關功能,以至于到后面重構(gòu)稍微有些困難,繼續(xù)寫下去恐成屎山,故在搭完射擊和武器UI后不再繼續(xù)泛化到敵人和敵人狀態(tài)機
本次主要完成了
自由配置武器參數(shù):武器所需的所有參數(shù)都可進行調(diào)整
武器的追隨準星:
根據(jù)輸入的武器槽位自動平滑的跟隨指定武器的槍線,換彈時也會有相應提示
武器的切換:
不同武器根據(jù)數(shù)字鍵進行切換,在UI和準星上也會有所體現(xiàn)
第一第三人稱的切換:
可以在第一第三人稱間無縫切換
請看VCR!
Unity機甲2
文章目錄
- 總覽
- 武器系統(tǒng)
- 子彈
- 第一第三人稱轉(zhuǎn)換
- UI
總覽
類圖結(jié)構(gòu)
武器系統(tǒng)
武器使用狀態(tài)機進行實現(xiàn),WeaponState繼承自State,內(nèi)部持有Weapon的引用
Weapon是掛載在玩家身上的主要類,一個武器對應一個Weapon,手動輸入索引標識其所屬的武器槽位
public class Weapon : MonoBehaviour
{public enum FireMode{Single,Brust,Auto}private Entity owner;public int weaponSelectIndex = 0;public bool selected { get; private set; } = false;[Header("武器性能")][SerializeField] private int maxAmmo = 1; //最大彈匣彈藥量[SerializeField] private int maxPrepareAmmo = 16; //最大后備彈藥量[SerializeField] private float shootingInterval = 0.1f; //射擊間隔[SerializeField] public float reloadTime = 2; //換彈時間[Header("開火模式")][SerializeField] public FireMode fireMode = FireMode.Single; //開火模式 [SerializeField] private int brustNum = 3; //brust一次開火射出的子彈數(shù)[SerializeField] private float brustTime = 1; //兩次brust開火之間的間隔//[Header("武器狀態(tài)參數(shù)")]private int curAmmo; //當前彈藥量private int curPrepareAmmo; //當前后備彈藥量private float shootingIntervalTimer = 0; //射擊間隔計時器private float brustTimeTimer = 0; //brust射擊間隔計時器private int brustCounter; //brust計數(shù)器[Header("發(fā)射物")][SerializeField] public Transform fireSocket; [SerializeField] private GameObject bulletPrefab; public float bulletVelocity = 100; public float inertialVelocityMultipler = 10; public bool constantSpeed = false; //本來想做個委托外包出去,想了想不如直接集成在類里得了[Header("特效效果")][SerializeField] private GameObject fireFX;private CinemachineImpulseSource impulseSource;public float cameraShakeMultipler = 1f;[Header("音頻")][SerializeField] private AudioClip fireSound;[SerializeField] private AudioClip reloadSound;[SerializeField] private float soundMultipler = 1f;//玩家的輸入對應的委托轉(zhuǎn)發(fā)public UnityAction onFireStart, onFiring, onFireEnd, onReload;//換彈時的委托,與UI通信使用public UnityAction onReloadStart, onReloadEnd;public UnityAction<float> onReloading;public UnityAction<int, int> onAmmoChanged;//是否選中public UnityAction<bool> onSelectChanged;//自己的狀態(tài)機private StateMachine stateMachine = new StateMachine();public WeaponIdleState idleState;public WeaponFireState fireState;public WeaponReloadState reloadState;private void Awake(){owner = GetComponent<Entity>();impulseSource = fireSocket.GetComponent<CinemachineImpulseSource>();if (weaponSelectIndex == 1)selected = true;//初始化數(shù)據(jù)curAmmo = maxAmmo;curPrepareAmmo = maxPrepareAmmo;brustTimeTimer = brustTime;brustCounter = brustNum;//狀態(tài)初始化idleState = new WeaponIdleState(stateMachine, this);fireState = new WeaponFireState(stateMachine, this);reloadState = new WeaponReloadState(stateMachine, this);//自身賦值到Controller方便其他組件引用PlayerController.Ins.weapons[weaponSelectIndex] = this;}private void Start(){//玩家操作本W(wǎng)eaponowner.onFireStart += () => onFireStart?.Invoke();owner.onFireEnd += () => onFireEnd?.Invoke();owner.onReload += () => onReload?.Invoke();owner.onSelect += (num) =>{selected = num == weaponSelectIndex;onSelectChanged?.Invoke(selected);};owner.onAllSelect += () =>{selected = true;onSelectChanged?.Invoke(selected);};//初始化狀態(tài)機stateMachine.Init(idleState);}private void Update(){stateMachine.Update();if (shootingIntervalTimer > 0)shootingIntervalTimer -= Time.deltaTime;if (brustCounter <= 0 && brustTimeTimer > 0){brustTimeTimer -= Time.deltaTime;if (brustTimeTimer <= 0)brustCounter = brustNum;}if(owner.firing)onFiring?.Invoke();}public void ModifyAmmo(int amount){curAmmo += amount;curAmmo = Mathf.Clamp(curAmmo, 0, maxAmmo);onAmmoChanged?.Invoke(curAmmo, curPrepareAmmo);}public void Fire(){if (!CanFire())return;//發(fā)射投射物Bullet bullet = Instantiate(bulletPrefab, fireSocket.position, fireSocket.rotation).GetComponent<Bullet>();bullet.Init(bulletVelocity, constantSpeed, new Vector3(owner.velocity.x, 0, owner.velocity.z) * inertialVelocityMultipler, owner.flag);//數(shù)據(jù)更新ModifyAmmo(-1);shootingIntervalTimer = shootingInterval;if (fireMode == FireMode.Brust) //如果是Brust模式{brustCounter--;if (brustCounter <= 0)brustTimeTimer = brustTime;}//播放槍口特效Instantiate(fireFX, fireSocket.position, fireSocket.rotation);//震動!impulseSource.m_DefaultVelocity.x = Random.Range(-1f, 1f) * cameraShakeMultipler;impulseSource.m_DefaultVelocity.y = Random.Range(-1f, 1f) * cameraShakeMultipler;impulseSource.m_DefaultVelocity.z = Random.Range(-1f, 1f) * cameraShakeMultipler;impulseSource.GenerateImpulse();//槍口音效AudioManager.PlayClipAtPoint(fireSound, fireSocket.position, soundMultipler);}public void Reload(){int needAmmo = maxAmmo - curAmmo;curAmmo = Mathf.Min(curPrepareAmmo, maxAmmo);curPrepareAmmo = Mathf.Max(curPrepareAmmo - needAmmo, 0);onAmmoChanged?.Invoke(curAmmo, curPrepareAmmo);AudioManager.PlayClipAtPoint(reloadSound, owner.transform.position, soundMultipler);}public bool HaveAmmo() => curAmmo > 0;public bool HavePrepareAmmo() => curPrepareAmmo > 0;public bool CanFire(){if (!selected)return false;//沒有子彈if (!HaveAmmo())return false;//沒有結(jié)束冷卻if (shootingIntervalTimer > 0)return false;//如果在Brust模式//如果Counter小于等于0,說明打完了,否則不管//如果打完了并且還沒過brust冷卻,那就不能打if (fireMode == FireMode.Brust && brustCounter <= 0 && brustTimeTimer > 0)return false;return true;}public bool CanBrust() => brustCounter > 0;public float GetReloadTime() => reloadTime;public int GetCurAmmo() => curAmmo;public int GetCurPrepareAmmo() => curPrepareAmmo;public bool CanReload() => curPrepareAmmo > 0 && curAmmo < maxAmmo;public State GetCurState() => stateMachine.curState;
}
武器的主要邏輯存在于狀態(tài)機中
但是由于武器的開火分為連發(fā),單發(fā),爆發(fā),因此還需要做一些特殊的處理
以下是WeaponFireState
在使用爆發(fā)模式時,即使玩家松開開火鍵也不能立即停止開火
public class WeaponFireState : WeaponCommonState
{public bool readyEnd;public WeaponFireState(StateMachine stateMachine, Weapon weapon) : base(stateMachine, weapon){}public override void Enter(){base.Enter();weapon.onFireEnd += OnFireEnd;FireOrReload();readyEnd = false;}public override void Exit(){base.Exit();weapon.onFireEnd -= OnFireEnd;}private void OnFireEnd(){if (weapon.fireMode != Weapon.FireMode.Brust)stateMachine.ChangeState(weapon.idleState);readyEnd = true;}public override void Update(){base.Update();//爆發(fā)模式if (weapon.fireMode == Weapon.FireMode.Brust){if (weapon.CanBrust())FireOrReload();else if(readyEnd)stateMachine.ChangeState(weapon.idleState);}else if (weapon.fireMode == Weapon.FireMode.Auto)FireOrReload();}private void FireOrReload(){if (weapon.HaveAmmo()){weapon.Fire();if (!weapon.HaveAmmo())stateMachine.ChangeState(weapon.reloadState);}elsestateMachine.ChangeState(weapon.reloadState);}
}
其他部分較為簡短不特別描述
子彈
子彈同樣使用一個通用的類進行配置
public class Bullet : MonoBehaviour
{[Header("Physical")]private Rigidbody rb;private Vector3 lastPosition;[Header("Attribute")]//子彈所屬派系,可以設定是否開啟友軍傷害,-1為中立派系public int flag = -1;public float velocity = 100;public bool constantSpeed = false;public float lifeTime = 6f;private float lifeTimer;public float gravityMultiper = 1f;[Header("VFX")]public GameObject explosionPrefab;public GameObject trailPrefab;private void Awake(){rb = GetComponent<Rigidbody>();lastPosition = transform.position;lifeTimer = lifeTime;}public void Init(float velocity, bool constantSpeed, Vector3 inertialVelocity /*慣性力*/, int flag = -1){this.velocity = velocity;this.constantSpeed = constantSpeed;rb.velocity += transform.forward * velocity * Time.fixedDeltaTime / rb.mass;rb.AddForce(transform.forward * velocity + inertialVelocity, ForceMode.Impulse);this.flag = flag;}private void Update(){lifeTimer -= Time.deltaTime;if (lifeTimer < 0)OnCollisionEnter(null);}void FixedUpdate(){//防止錯過剛體,對即將經(jīng)過的間隔做一個射線檢測if (Physics.Raycast(lastPosition, rb.velocity.normalized, out RaycastHit hitInfo, rb.velocity.magnitude * Time.fixedDeltaTime)){transform.position = hitInfo.point;rb.velocity = Vector3.zero;return;}//持久動力if (constantSpeed)rb.AddForce(transform.forward * velocity);//調(diào)整旋轉(zhuǎn)朝向transform.forward = rb.velocity.normalized;//應用重力乘數(shù)rb.velocity += new Vector3(0, 9.8f * (1 - gravityMultiper) * Time.fixedDeltaTime, 0);//記錄位置lastPosition = transform.position;}private void OnCollisionEnter(Collision collision){if(collision != null && collision.gameObject.TryGetComponent(out Entity entity)){if (entity.flag == flag)return;}if (trailPrefab){trailPrefab.transform.parent = null;var particleSystems = trailPrefab.GetComponentsInChildren<ParticleSystem>();foreach (var particle in particleSystems){var main = particle.main;main.loop = false;}}Instantiate(explosionPrefab, transform.position, Quaternion.identity);Destroy(gameObject);}private void DelayTrail(){trailPrefab.SetActive(true);}
}
其內(nèi)部包含初始慣性處理,持續(xù)動力,防止高速穿過物體的處理以及視覺和銷毀時如果有拖尾的處理
第一第三人稱轉(zhuǎn)換
這一塊比較簡單,直接使用Cinemachine自帶的混合,代碼只需要控制兩個虛擬相機的激活即可
public class PlayerCameraController : MonoBehaviour
{//Third Person Camera[SerializeField] private CinemachineVirtualCamera thirdPersonCamera;private Cinemachine3rdPersonFollow thirdCameraBody;public float freeLookSide = 0;public float freeLookDistance = 20;float cameraSide;float cameraDistance;//First Peroson Camera[SerializeField] private CinemachineVirtualCamera firstPersonCamera;private Cinemachine3rdPersonFollow firstCameraBody;private Quaternion lastQuaternion;private void Awake(){thirdCameraBody = thirdPersonCamera.GetCinemachineComponent<Cinemachine3rdPersonFollow>();cameraSide = thirdCameraBody.CameraSide;cameraDistance = thirdCameraBody.CameraDistance;firstCameraBody = firstPersonCamera.GetCinemachineComponent<Cinemachine3rdPersonFollow>();}// Update is called once per framevoid Update(){HandleFreeLook();HandleSwitchView();}private void HandleFreeLook(){if (Input.GetKeyDown(KeyCode.C)){//控制器凍結(jié)lastQuaternion = PlayerController.GetControllerRotation();PlayerController.SetPause(true);//第三人稱參數(shù)thirdCameraBody.CameraSide = freeLookSide;thirdCameraBody.CameraDistance = freeLookDistance;//隱藏準星(有視覺Bug)foreach(var hair in UIManager.Ins.crossHairs){hair.gameObject.SetActive(false);}}if (Input.GetKeyUp(KeyCode.C)){//控制器恢復PlayerController.SetControllerRotation(lastQuaternion);PlayerController.SetPause(false);//第三人稱參數(shù)thirdCameraBody.CameraSide = cameraSide;thirdCameraBody.CameraDistance = cameraDistance;//顯示UIforeach (var hair in UIManager.Ins.crossHairs){hair.gameObject.SetActive(PlayerController.Ins.weapons[hair.weaponIndex].selected);}}}private void HandleSwitchView(){if (Input.GetKeyDown(KeyCode.V)){firstPersonCamera.gameObject.SetActive(!firstPersonCamera.gameObject.activeSelf);thirdPersonCamera.gameObject.SetActive(!thirdPersonCamera.gameObject.activeSelf);}}
}
UI
比較重要的地方是絲滑的UI跟隨以及實時的武器欄
后者只需要在制作時留意委托就可以很方便的調(diào)用,前者則需要一些不同空間的變換知識
準星的跟隨部分
一開始我在Canvas中選擇的渲染模式是覆蓋,后來發(fā)現(xiàn)在覆蓋的模式下不能添加自發(fā)光,導致UI較暗,于是調(diào)整為了攝像機空間,但調(diào)整后導致原本跟蹤正確的準星又不再正確,下面是解決辦法
public class CrossHairUI : MonoBehaviour
{//自身private RectTransform rect, parent;[SerializeField] private GameObject aimHair, reloadHair;[SerializeField] private TextMeshPro reloadTxt;//武器public int weaponIndex = 0;private Weapon weapon;private Transform fireSocket;public float lerpMultipler = 0.1f;void Start(){rect = GetComponent<RectTransform>();parent = rect.parent.GetComponent<RectTransform>();weapon = PlayerController.Ins.weapons[weaponIndex];fireSocket = weapon.fireSocket;weapon.onReloadStart += OnReloadStart;weapon.onReloading += OnReloading;weapon.onReloadEnd += OnReloadEnd;weapon.onSelectChanged += (selected) =>{gameObject.SetActive(selected);};gameObject.SetActive(weapon.selected);}void Update(){if (Physics.SphereCast(fireSocket.position, .5f, fireSocket.forward, out RaycastHit hitInfo, 1000)){if(RectTransformUtility.ScreenPointToLocalPointInRectangle(parent, RectTransformUtility.WorldToScreenPoint(Camera.main, hitInfo.point), Camera.main, out Vector2 localPoint)){rect.localPosition = Vector2.Lerp(rect.localPosition, localPoint, lerpMultipler);}}else{Vector3 point = fireSocket.position + fireSocket.forward * 3000;if (RectTransformUtility.ScreenPointToLocalPointInRectangle(parent, RectTransformUtility.WorldToScreenPoint(Camera.main, point), Camera.main, out Vector2 localPoint)){rect.localPosition = Vector2.Lerp(rect.localPosition, localPoint, lerpMultipler);}}}private void OnReloadStart(){aimHair.SetActive(false);reloadHair.SetActive(true);}private void OnReloading(float remainTime){reloadTxt.SetText(remainTime.ToString("0.00"));}private void OnReloadEnd(){aimHair.SetActive(true);reloadHair.SetActive(false);}
}
在Update中首先SphereCast來獲取擊中的點,再將其WorldToScreenPoint變換到屏幕空間,如果是覆蓋的渲染模式,此時已經(jīng)結(jié)束了,但由于是攝像機模式,因此需要再多一個變換即ScreenPointToLocalPointInRectangle將其變換到面板上的相對位置。之后使用插值即可實現(xiàn)絲滑的跟蹤準