Files
megakoop/Game/Scripts/Networking/SteamCharacterNetworkBridge.cs
2025-10-28 12:09:49 +01:00

459 lines
15 KiB
C#

using Steamworks;
using UnityEngine;
namespace MegaKoop.Game.Networking
{
[DisallowMultipleComponent]
public class SteamCharacterNetworkBridge : MonoBehaviour
{
[Header("References")]
[SerializeField] private ThirdPersonCharacterController characterController;
[SerializeField] private Animator animator;
[SerializeField] private NetworkIdentity identity;
[SerializeField] private Transform rootTransform;
[SerializeField] private NetworkCharacterInputProxy networkInputProxy;
[Header("Settings")]
[SerializeField] private float transformBroadcastInterval = 0.05f;
[SerializeField] private float remoteLerpSpeed = 12f;
[SerializeField] private ulong ownerSteamId;
[SerializeField] private bool autoAssignOwnerToLocalPlayer = true;
private SteamCoopNetworkManager networkManager;
private float broadcastTimer;
private bool isRegistered;
private bool isLocalPlayer;
private bool isAuthority;
private Vector3 remoteTargetPosition;
private Quaternion remoteTargetRotation;
private Vector3 remoteTargetVelocity;
private bool haveRemoteState;
private bool localOverrideSet;
private int expectedNetworkId;
public void AssignOwner(ulong steamId, bool localPlayer)
{
ownerSteamId = steamId;
isLocalPlayer = localPlayer;
localOverrideSet = true;
CaptureExpectedNetworkId();
UpdateAuthority();
ConfigureController();
}
public bool IsLocalPlayer => isLocalPlayer;
public bool IsAuthority => isAuthority;
public ulong OwnerSteamId => ownerSteamId;
private void Awake()
{
if (characterController == null)
{
characterController = GetComponent<ThirdPersonCharacterController>();
}
if (identity == null)
{
identity = GetComponent<NetworkIdentity>();
}
if (rootTransform == null)
{
rootTransform = transform;
}
if (networkInputProxy == null)
{
networkInputProxy = GetComponent<NetworkCharacterInputProxy>();
if (networkInputProxy == null)
{
networkInputProxy = gameObject.AddComponent<NetworkCharacterInputProxy>();
networkInputProxy.hideFlags = HideFlags.HideInInspector;
}
}
if (animator == null)
{
animator = GetComponent<Animator>();
if (animator != null)
{
animator.cullingMode = AnimatorCullingMode.AlwaysAnimate;
}
}
remoteTargetPosition = rootTransform.position;
remoteTargetRotation = rootTransform.rotation;
}
private void Start()
{
networkManager = SteamCoopNetworkManager.Instance;
CaptureExpectedNetworkId();
UpdateAuthority();
ConfigureController();
}
private void OnEnable()
{
networkManager = SteamCoopNetworkManager.Instance;
CaptureExpectedNetworkId();
UpdateAuthority();
ConfigureController();
RegisterHandlers();
}
private void OnDisable()
{
UnregisterHandlers();
}
private void Update()
{
if (networkManager == null)
{
networkManager = SteamCoopNetworkManager.Instance;
if (networkManager != null)
{
RegisterHandlers();
}
}
if (networkManager == null)
{
return;
}
if (isAuthority)
{
broadcastTimer -= Time.deltaTime;
if (broadcastTimer <= 0f)
{
BroadcastTransform();
broadcastTimer = transformBroadcastInterval;
}
}
else
{
if (haveRemoteState)
{
rootTransform.position = Vector3.Lerp(rootTransform.position, remoteTargetPosition, remoteLerpSpeed * Time.deltaTime);
rootTransform.rotation = Quaternion.Slerp(rootTransform.rotation, remoteTargetRotation, remoteLerpSpeed * Time.deltaTime);
}
}
}
private void RegisterHandlers()
{
if (networkManager == null || isRegistered)
{
return;
}
networkManager.RegisterHandler(NetworkMessageType.PlayerInput, HandlePlayerInputMessage);
networkManager.RegisterHandler(NetworkMessageType.CharacterTransform, HandleCharacterTransformMessage);
networkManager.RegisterHandler(NetworkMessageType.CharacterAnim, HandleCharacterAnimMessage);
isRegistered = true;
}
private void UnregisterHandlers()
{
if (networkManager == null || !isRegistered)
{
return;
}
networkManager.UnregisterHandler(NetworkMessageType.PlayerInput, HandlePlayerInputMessage);
networkManager.UnregisterHandler(NetworkMessageType.CharacterTransform, HandleCharacterTransformMessage);
networkManager.UnregisterHandler(NetworkMessageType.CharacterAnim, HandleCharacterAnimMessage);
isRegistered = false;
}
private void UpdateAuthority()
{
if (networkManager == null)
{
networkManager = SteamCoopNetworkManager.Instance;
}
ulong localSteamId = SteamBootstrap.IsInitialized ? SteamUser.GetSteamID().m_SteamID : 0UL;
if (!localOverrideSet && autoAssignOwnerToLocalPlayer && ownerSteamId == 0 && localSteamId != 0)
{
ownerSteamId = localSteamId;
}
if (!localOverrideSet)
{
isLocalPlayer = ownerSteamId != 0 && ownerSteamId == localSteamId;
}
isAuthority = isLocalPlayer; // Each player has authority over their own character.
}
private void ConfigureController()
{
if (characterController == null)
{
return;
}
if (isAuthority)
{
characterController.enabled = true;
characterController.SetInputSource(isLocalPlayer ? null : networkInputProxy);
var unityController = characterController.GetComponent<UnityEngine.CharacterController>();
if (unityController != null)
{
unityController.enabled = true;
unityController.detectCollisions = true;
unityController.enableOverlapRecovery = true;
}
}
else
{
characterController.enabled = false;
characterController.SetInputSource(null);
var unityController = characterController.GetComponent<UnityEngine.CharacterController>();
if (unityController != null)
{
unityController.enabled = true;
unityController.detectCollisions = true;
unityController.enableOverlapRecovery = false;
}
}
}
private void BroadcastTransform()
{
if (identity == null)
{
return;
}
if (identity.NetworkId == 0)
{
return;
}
var unityController = GetComponent<CharacterController>();
Vector3 velocity = unityController != null ? unityController.velocity : Vector3.zero;
SteamCharacterStateCache.ReportLocalState(identity.NetworkId, rootTransform.position, rootTransform.rotation, velocity);
var message = new CharacterTransformMessage(identity.NetworkId, rootTransform.position, rootTransform.rotation, velocity);
byte[] payload = CharacterTransformMessage.Serialize(message);
networkManager.SendToAll(NetworkMessageType.CharacterTransform, payload, EP2PSend.k_EP2PSendUnreliableNoDelay);
BroadcastAnimatorParameters();
}
private void BroadcastAnimatorParameters()
{
if (networkManager == null || identity == null || animator == null)
{
return;
}
if (identity.NetworkId == 0)
{
return;
}
float moveX = animator.GetFloat("MoveX");
float moveZ = animator.GetFloat("MoveZ");
float speed = animator.GetFloat("Speed");
float moveSpeedNorm = animator.GetFloat("MoveSpeedNormalized");
bool isGround = animator.GetBool("IsGrounded");
bool isCrouch = animator.GetBool("IsCrouching");
bool isDeadFlag = animator.GetBool("IsDead");
bool isJump = animator.GetBool("IsJumping");
var animMsg = new CharacterAnimMessage(
identity.NetworkId,
moveX,
moveZ,
speed,
moveSpeedNorm,
isGround,
isCrouch,
isDeadFlag,
isJump);
byte[] animPayload = CharacterAnimMessage.Serialize(animMsg);
networkManager.SendToAll(NetworkMessageType.CharacterAnim, animPayload, EP2PSend.k_EP2PSendUnreliableNoDelay);
}
private void HandlePlayerInputMessage(NetworkMessage message)
{
if (isAuthority)
{
return;
}
PlayerInputMessage inputMessage = PlayerInputMessage.Deserialize(message.Payload);
if (inputMessage.NetworkId != identity.NetworkId)
{
return;
}
networkInputProxy.SetInput(inputMessage.MoveInput, inputMessage.JumpPressed);
}
private void HandleCharacterTransformMessage(NetworkMessage message)
{
if (isAuthority)
{
return;
}
CharacterTransformMessage transformMessage = CharacterTransformMessage.Deserialize(message.Payload);
if (!EnsureMatchingNetworkId(transformMessage.NetworkId, message.Sender))
{
return;
}
remoteTargetPosition = transformMessage.Position;
remoteTargetRotation = transformMessage.Rotation;
remoteTargetVelocity = transformMessage.Velocity;
haveRemoteState = true;
}
private void HandleCharacterAnimMessage(NetworkMessage message)
{
if (isAuthority)
{
return;
}
CharacterAnimMessage anim = CharacterAnimMessage.Deserialize(message.Payload);
if (!EnsureMatchingNetworkId(anim.NetworkId, message.Sender))
{
return;
}
if (animator == null)
{
animator = GetComponent<Animator>();
if (animator == null)
{
return;
}
}
animator.SetFloat("MoveX", anim.MoveX);
animator.SetFloat("MoveZ", anim.MoveZ);
animator.SetFloat("Speed", anim.Speed);
animator.SetFloat("MoveSpeedNormalized", anim.MoveSpeedNormalized);
animator.SetBool("IsGrounded", anim.IsGrounded);
animator.SetBool("IsCrouching", anim.IsCrouching);
animator.SetBool("IsDead", anim.IsDead);
animator.SetBool("IsJumping", anim.IsJumping);
}
private void CaptureExpectedNetworkId()
{
if (identity == null)
{
identity = GetComponent<NetworkIdentity>();
}
if (identity == null)
{
return;
}
if (identity.NetworkId != 0)
{
expectedNetworkId = identity.NetworkId;
if (ownerSteamId != 0)
{
var mapped = NetworkIdRegistry.GetNetworkIdForSteamId(ownerSteamId);
if (mapped != expectedNetworkId)
{
NetworkIdRegistry.MapSteamIdToNetworkId(ownerSteamId, expectedNetworkId);
}
}
}
}
private bool EnsureMatchingNetworkId(int incomingId, ulong sender)
{
if (identity == null)
{
return false;
}
if (incomingId == 0)
{
return false;
}
int currentId = identity.NetworkId;
if (currentId == incomingId)
{
return true;
}
if (currentId != 0)
{
return false;
}
if (ownerSteamId != 0 && sender != ownerSteamId)
{
return false;
}
int mappedId = ownerSteamId != 0 ? NetworkIdRegistry.GetNetworkIdForSteamId(ownerSteamId) : expectedNetworkId;
if (mappedId != 0)
{
identity.SetNetworkId(mappedId);
if (identity.NetworkId == mappedId)
{
expectedNetworkId = mappedId;
return mappedId == incomingId;
}
return false;
}
if (!NetworkIdAllocator.IsPlayerId(incomingId))
{
return false;
}
identity.SetNetworkId(incomingId);
if (identity.NetworkId != incomingId)
{
return false;
}
expectedNetworkId = incomingId;
if (ownerSteamId != 0)
{
NetworkIdRegistry.MapSteamIdToNetworkId(ownerSteamId, incomingId);
}
return true;
}
public void SendLocalInput(Vector2 moveInput, bool jump)
{
if (networkManager == null || identity == null || !isAuthority)
{
return;
}
if (!networkManager.IsConnected)
{
return;
}
if (identity.NetworkId == 0)
{
return;
}
networkManager.SendToAll(NetworkMessageType.PlayerInput, PlayerInputMessage.Serialize(new PlayerInputMessage(identity.NetworkId, moveInput, jump)), EP2PSend.k_EP2PSendUnreliableNoDelay);
}
}
}