射击特效
上节课在编写切换武器代码时,判断了用户是否是Host,如果是Host,要避免执行两次武器切换的操作。同样,在之前死亡的代码中,我们同时执行了 DieOnServer()
和 DieOnClient()
,这里也需要判断一下当前用户是否是Host,如果是Host,就不需要执行 DieOnServer()
了。
if (!IsHost)
{
DieOnServer();
}
DieClientRpc();
今天主要来给枪械加上枪口火焰特效和击中物体后的粒子特效
导入特效包
跟之前的资源一样导入,出现下面的提示框,点击升级即可
直接点击 import
保存一下当前场景。在刚刚下好的包 Unity Technologies -> Particle Pack -> Scenes
里,双击 Main
,可以进入这个包提供好的粒子特效场景中,浏览各种粒子特效的效果。
添加特效
在特效中,选择 Unity Technologies -> Particle Pack -> Effect Examples -> Weapon Effects -> Prefabs
里的 MuzzleFlash01
, 还有同目录下的 MetalImpacts StoneImpacts
里的 HitEffect
物体拖进我们的 Prefabs
里。
添加枪口火焰
先添加长枪的枪口火焰。点击 Prefabs
里的长枪,添加一个子空物体 FirePoint
将火焰特效拖到 FirePoint
上,并把坐标重置
调整枪口火焰的位置、朝向和大小
在编辑器左边有特效的设置,Looping
是特效是否重复播放,我们希望特效在开枪时才出现,因此关掉;Play On Awake
是特效是否在物体被加入进去时就播放,我们也不希望,关掉。
其他枪械枪口火焰设置同理。
枪口火焰逻辑
编写脚本 WeaponGraphics
控制枪口火焰的产生逻辑。控制的火焰包括枪口火焰和击中的各种物体的特效,比如这里包括击中地板的特效和击中障碍物的特效。
枪口火焰位置确定,使用 ParticleSystem
类型的变量引用。击中物体的特效位置不确定,需要即时渲染,使用 GameObject
。
在枪械中添加 WeaponGraphics
, 将特效加到变量上。
修改 WeaponManager
我们需要一个变量来存储当前武器的特效,并在获取当前武器后将其取得。由于 WeaponGraphics
是武器上的一个组件,可以使用 GetComponent
获取。
currentWeaponGraphics = weaponObject.GetComponent<WeaponGraphics>();
修改 PlayerShooting
我们在 Shoot()
时渲染枪口火花。编写辅助函数 OnShoot()
展示射击相关的逻辑,包括特效、声音等,每次射击时被调用。射击相关的逻辑需要网络同步,同步运行逻辑和之前的网络同步逻辑相同,都是现在本地玩家处执行,然后发送到服务器执行,再发送到其他所有玩家处执行,注意防止Host重复渲染。
private void OnShoot() // 每次射击相关的逻辑,包括特效、声音等
{
}
[ServerRpc]
private void OnShootServerRpc()
{
if (!IsHost)
{
OnShoot();
}
OnShootClientRpc();
}
[ClientRpc]
private void OnShootClientRpc()
{
OnShoot();
}
先前非本地玩家的 PlayerShooting
被禁用,现在需要同步渲染,因此要解禁(PlayerSetup
里面禁用的,找到 Player
的该组件,右键选择从数组里删除)。然后在接收操作的 Update()
函数里判断,如果不是本地玩家就不接收操作。
在 OnShoot()
里播放枪口火焰特效
weaponManager.GetCurrentWeaponGraphics().muzzleFlash.Play();
目前存在一个BUG,就是当我们在连发时切枪,连发效果不会消失,只有当我们切回连发的枪并松开鼠标左键才会消失。这是因为我们判断连发消失的条件只有松开鼠标左键。我们可以在判断条件中添加一个判断是否切枪。
添加击中物体特效
之前选择的击中物体特效形状都是圆形,可以在右边的 Particle System
里找到 Shape
,选择 Sprite
改成点
设置里的 Looping
和 Play On Awake
也要取消,并将特效持续时间 Duration
改成 0.25秒。
击中物体特效逻辑
使用变量存储击中的特效种类
enum HitEffectMaterial
{
Metal,
Stone,
}
使用 OnHit()
函数来渲染击中特效,需要网络通信,因此也需要 OnHitServerRpc()
和 OnHitClientRpc()
在 Shoot()
函数中,当击中玩家时,产生金属特效
OnHitServerRpc(hit.point, hit.normal, HitEffectMaterial.Metal);
编写 OnHit()
private void OnHit(Vector3 pos, Vector3 normal, HitEffectMaterial material) // 击中点的特效,传递的值是位置、法向量和材质
{
GameObject hitEffectPrefab;
if (material == HitEffectMaterial.Metal)
{
hitEffectPrefab = weaponManager.GetCurrentWeaponGraphics().metalHitEffectPrefab;
}
else if (material == HitEffectMaterial.Stone)
{
hitEffectPrefab = weaponManager.GetCurrentWeaponGraphics().stoneHitEffectPrefab;
}
else
{
hitEffectPrefab = new GameObject();
}
// 将特效实例化,传递信息,其中法向量要传递反向的向量,也是人眼看过来的向量方向
GameObject hitEffectObject = Instantiate(hitEffectPrefab, pos, Quaternion.LookRotation(normal));
ParticleSystem particleSystem = hitEffectObject.GetComponent<ParticleSystem>();
particleSystem.Emit(1); // 使特效立即触发
particleSystem.Play();
Destroy(hitEffectObject, 0.25f); // 在0.25秒后将特效删除
}
阶段性成品
修改的代码
PlayerShooting.cs
using System.Collections;
using System.Collections.Generic;
using Unity.Netcode;
using UnityEngine;
public class PlayerShooting : NetworkBehaviour
{
private const string PLAYER_TAG = "Player";
private WeaponManager weaponManager;
private PlayerWeapon currentWeapon;
[SerializeField]
private LayerMask mask;
private Camera cam; // 角色摄像头
enum HitEffectMaterial
{
Metal,
Stone,
}
// Start is called before the first frame update
void Start()
{
cam = GetComponentInChildren<Camera>();
weaponManager = GetComponent<WeaponManager>();
}
// Update is called once per frame
void Update()
{
if (!IsLocalPlayer) return;
currentWeapon = weaponManager.GetCurrentWeapon();
if (currentWeapon.shootRate <= 0) // 单发
{
if (Input.GetButtonDown("Fire1"))
{
Shoot();
}
}
else
{
if (Input.GetButtonDown("Fire1"))
{
InvokeRepeating("Shoot", 0f, 1f / currentWeapon.shootRate);
}
else if (Input.GetButtonUp("Fire1") || Input.GetKeyDown(KeyCode.Q))
{
CancelInvoke("Shoot");
}
}
}
private void OnHit(Vector3 pos, Vector3 normal, HitEffectMaterial material) // 击中点的特效,传递的值是位置、法向量和材质
{
GameObject hitEffectPrefab;
if (material == HitEffectMaterial.Metal)
{
hitEffectPrefab = weaponManager.GetCurrentWeaponGraphics().metalHitEffectPrefab;
}
else if (material == HitEffectMaterial.Stone)
{
hitEffectPrefab = weaponManager.GetCurrentWeaponGraphics().stoneHitEffectPrefab;
}
else
{
hitEffectPrefab = new GameObject();
}
// 将特效实例化,传递信息,其中法向量要传递反向的向量,也是人眼看过来的向量方向
GameObject hitEffectObject = Instantiate(hitEffectPrefab, pos, Quaternion.LookRotation(normal));
ParticleSystem particleSystem = hitEffectObject.GetComponent<ParticleSystem>();
particleSystem.Emit(1); // 使特效立即触发
particleSystem.Play();
Destroy(hitEffectObject, 0.25f); // 在0.25秒后将特效删除
}
[ClientRpc]
private void OnHitClientRpc(Vector3 pos, Vector3 normal, HitEffectMaterial material)
{
OnHit(pos, normal, material);
}
[ServerRpc]
private void OnHitServerRpc(Vector3 pos, Vector3 normal, HitEffectMaterial material)
{
if (!IsHost)
{
OnHit(pos, normal, material);
}
OnHitClientRpc(pos, normal, material);
}
private void OnShoot() // 每次射击相关的逻辑,包括特效、声音等
{
weaponManager.GetCurrentWeaponGraphics().muzzleFlash.Play(); // 播放枪口火焰特效
}
[ServerRpc]
private void OnShootServerRpc()
{
if (!IsHost)
{
OnShoot();
}
OnShootClientRpc();
}
[ClientRpc]
private void OnShootClientRpc()
{
OnShoot();
}
private void Shoot()
{
OnShootServerRpc();
RaycastHit hit;
if (Physics.Raycast(cam.transform.position, cam.transform.forward, out hit, currentWeapon.range, mask))
{
if (hit.collider.tag == PLAYER_TAG)
{
ShootServerRpc(hit.collider.name, currentWeapon.damage);
OnHitServerRpc(hit.point, hit.normal, HitEffectMaterial.Metal);
}
else
{
OnHitServerRpc(hit.point, hit.normal, HitEffectMaterial.Stone);
}
}
}
[ServerRpc]
private void ShootServerRpc(string name, int damage)
{
Player player = GameManager.Singleton.GetPlayer(name);
player.TakeDamage(damage);
}
}
WeaponGraphics.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class WeaponGraphics : MonoBehaviour
{
public ParticleSystem muzzleFlash; // 枪口火花
public GameObject metalHitEffectPrefab; // 击中金属的特效
public GameObject stoneHitEffectPrefab; // 击中石头的特效
}
WeaponManager.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Unity.Netcode;
public class WeaponManager : NetworkBehaviour
{
[SerializeField]
private PlayerWeapon primaryWeapon;
[SerializeField]
private PlayerWeapon secondaryWeapon;
private NetworkVariable<int> currentWeaponIndex = new NetworkVariable<int>(); // 记录玩家当前持的枪,初始值为0
[SerializeField]
private GameObject weaponHolder;
private PlayerWeapon currentWeapon;
private WeaponGraphics currentWeaponGraphics;
public override void OnNetworkSpawn()
{
base.OnNetworkSpawn();
EquipWeapon(currentWeaponIndex.Value);
}
public void EquipWeapon(int weaponIndex)
{
if (weaponIndex == 0)
{
currentWeapon = primaryWeapon;
}
else if (weaponIndex == 1)
{
currentWeapon = secondaryWeapon;
}
if (weaponHolder.transform.childCount > 0)
{
Destroy(weaponHolder.transform.GetChild(0).gameObject); // 去掉先前挂载的枪的gameObject
}
// 创建渲染一个武器物体对象
GameObject weaponObject = Instantiate(currentWeapon.graphics, weaponHolder.transform.position, weaponHolder.transform.rotation);
weaponObject.transform.SetParent(weaponHolder.transform); // 将武器对象挂载到WeaponHolder上
currentWeaponGraphics = weaponObject.GetComponent<WeaponGraphics>();
}
public PlayerWeapon GetCurrentWeapon()
{
return currentWeapon;
}
public WeaponGraphics GetCurrentWeaponGraphics()
{
return currentWeaponGraphics;
}
private void ToggleWeapon(int weaponIndex)
{
EquipWeapon(weaponIndex);
}
[ClientRpc]
private void ToggleWeaponClientRpc(int weaponIndex)
{
ToggleWeapon(weaponIndex);
}
[ServerRpc]
private void ToggleWeaponServerRpc()
{
currentWeaponIndex.Value = (currentWeaponIndex.Value + 1) % 2; // 改变数值来切枪,只有两把枪
if (!IsHost)
{
ToggleWeapon(currentWeaponIndex.Value); // 这里一定要传入数值,因为客户端和服务器端的网络变量数值不一定同步,会出现切枪错误的问题,所以这里直接传修改好的值
}
ToggleWeaponClientRpc(currentWeaponIndex.Value); // 同理
}
public void SetDefaultWeapon()
{
currentWeaponIndex.Value = 0;
}
// Update is called once per frame
void Update()
{
if (IsLocalPlayer)
{
if (Input.GetKeyDown(KeyCode.Q)) // 按下Q切换武器
{
ToggleWeaponServerRpc();
}
}
}
}
Player.cs
using System.Collections;
using System.Collections.Generic;
using Unity.Netcode;
using UnityEngine;
public class Player : NetworkBehaviour
{
[SerializeField]
private int maxHealth = 100;
private NetworkVariable<int> currentHealth = new NetworkVariable<int>();
private NetworkVariable<bool> isDead = new NetworkVariable<bool>();
[SerializeField]
private Behaviour[] componentsToDisable; // 要禁用的组件
private bool[] componentsEnabled; // 组件初始状态
private bool colliderEnabled; // 碰撞体初始状态
public void Setup()
{
// 记录组件状态
componentsEnabled = new bool[componentsToDisable.Length];
for (int i = 0; i < componentsToDisable.Length; i++)
{
componentsEnabled[i] = componentsToDisable[i].enabled;
}
Collider col = GetComponent<Collider>();
colliderEnabled = col.enabled;
SetDefaults();
}
private void SetDefaults()
{
// 恢复组件状态
for (int i = 0;i < componentsToDisable.Length; i++)
{
componentsToDisable[i].enabled = componentsEnabled[i];
}
Collider col = GetComponent<Collider>();
col.enabled = colliderEnabled;
if (IsServer) // 这里要修改currentHealth,只有在Server执行才有效
{
currentHealth.Value = maxHealth;
isDead.Value = false;
}
}
public void TakeDamage(int damage) // 受到了伤害,只在服务器端被调用
{
if (isDead.Value) // 已经死亡就不调用死亡的逻辑
{
return;
}
currentHealth.Value -= damage;
if (currentHealth.Value <= 0)
{
currentHealth.Value = 0;
isDead.Value = true;
if (!IsHost) // Host不需要执行这一段,不然会重复执行
{
DieOnServer();
}
DieClientRpc();
}
}
private IEnumerator Respawn() // 重生
{
yield return new WaitForSeconds(GameManager.Singleton.MatchingSettings.respawnTime); // 停顿3秒,之后执行下面的语句
SetDefaults();
if (IsLocalPlayer) // 位置信息以本地玩家为准
{
transform.position = new Vector3(0f, 10f, 0f);
}
}
private void DieOnServer() // Server端需要单独执行一次Die的逻辑
{
Die();
}
[ClientRpc]
private void DieClientRpc() // 在Server端执行每个Client端上的函数
{
Die();
}
private void Die()
{
for (int i = 0; i < componentsToDisable.Length; i++)
{
componentsToDisable[i].enabled = false;
}
Collider col = GetComponent<Collider>();
col.enabled = false;
StartCoroutine(Respawn());
}
public int GetHealth()
{
return currentHealth.Value;
}
}
著作权信息
- 源视频作者:yxc
- 文章作者: jaylenwanghitsz
- 链接:https://www.acwing.com/file_system/file/content/whole/index/content/8510831/
- 来源:AcWing
- 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。