Characters
This commit is contained in:
10
Game/Scripts/Networking/ICharacterInputSource.cs
Normal file
10
Game/Scripts/Networking/ICharacterInputSource.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace MegaKoop.Game.Networking
|
||||
{
|
||||
public interface ICharacterInputSource
|
||||
{
|
||||
Vector2 MoveInput { get; }
|
||||
bool JumpPressed { get; }
|
||||
}
|
||||
}
|
||||
2
Game/Scripts/Networking/ICharacterInputSource.cs.meta
Normal file
2
Game/Scripts/Networking/ICharacterInputSource.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 403a3945388ae5082bf455368a91a800
|
||||
34
Game/Scripts/Networking/NetworkCharacterInputProxy.cs
Normal file
34
Game/Scripts/Networking/NetworkCharacterInputProxy.cs
Normal file
@@ -0,0 +1,34 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace MegaKoop.Game.Networking
|
||||
{
|
||||
public class NetworkCharacterInputProxy : MonoBehaviour, ICharacterInputSource
|
||||
{
|
||||
public Vector2 MoveInput { get; private set; }
|
||||
public bool JumpPressed { get; private set; }
|
||||
|
||||
private bool jumpConsumed;
|
||||
|
||||
public void SetInput(Vector2 move, bool jump)
|
||||
{
|
||||
MoveInput = move;
|
||||
if (jump)
|
||||
{
|
||||
JumpPressed = true;
|
||||
jumpConsumed = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void LateUpdate()
|
||||
{
|
||||
if (JumpPressed && !jumpConsumed)
|
||||
{
|
||||
jumpConsumed = true;
|
||||
}
|
||||
else if (jumpConsumed)
|
||||
{
|
||||
JumpPressed = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e3118c9c432a7acbd824645749251552
|
||||
53
Game/Scripts/Networking/NetworkIdentity.cs
Normal file
53
Game/Scripts/Networking/NetworkIdentity.cs
Normal file
@@ -0,0 +1,53 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace MegaKoop.Game.Networking
|
||||
{
|
||||
[DisallowMultipleComponent]
|
||||
public class NetworkIdentity : MonoBehaviour
|
||||
{
|
||||
private static readonly Dictionary<int, NetworkIdentity> registry = new();
|
||||
private static int nextId = 1;
|
||||
|
||||
[SerializeField] private int networkId;
|
||||
[SerializeField] private bool assignOnAwake = true;
|
||||
|
||||
public int NetworkId => networkId;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
if (assignOnAwake && networkId == 0)
|
||||
{
|
||||
networkId = nextId++;
|
||||
}
|
||||
|
||||
Register();
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
if (registry.TryGetValue(networkId, out NetworkIdentity existing) && existing == this)
|
||||
{
|
||||
registry.Remove(networkId);
|
||||
}
|
||||
}
|
||||
|
||||
private void Register()
|
||||
{
|
||||
if (networkId == 0)
|
||||
{
|
||||
Debug.LogWarning($"[NetworkIdentity] {name} has no network id and won't be tracked.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (registry.TryGetValue(networkId, out NetworkIdentity existing) && existing != this)
|
||||
{
|
||||
Debug.LogWarning($"[NetworkIdentity] Duplicate network id {networkId} detected. Overwriting reference.");
|
||||
}
|
||||
|
||||
registry[networkId] = this;
|
||||
}
|
||||
|
||||
public static bool TryGet(int id, out NetworkIdentity identity) => registry.TryGetValue(id, out identity);
|
||||
}
|
||||
}
|
||||
2
Game/Scripts/Networking/NetworkIdentity.cs.meta
Normal file
2
Game/Scripts/Networking/NetworkIdentity.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1e6f99ca4475ead269829ba672213d5c
|
||||
222
Game/Scripts/Networking/NetworkMessages.cs
Normal file
222
Game/Scripts/Networking/NetworkMessages.cs
Normal file
@@ -0,0 +1,222 @@
|
||||
using System;
|
||||
using UnityEngine;
|
||||
|
||||
namespace MegaKoop.Game.Networking
|
||||
{
|
||||
public enum NetworkMessageType : byte
|
||||
{
|
||||
Heartbeat = 0,
|
||||
LobbyState = 1,
|
||||
PlayerInput = 2,
|
||||
CharacterTransform = 3,
|
||||
WeaponFire = 4,
|
||||
HealthSync = 5,
|
||||
ProjectileSpawn = 6
|
||||
}
|
||||
|
||||
public readonly struct NetworkMessage
|
||||
{
|
||||
public readonly NetworkMessageType Type;
|
||||
public readonly byte[] Payload;
|
||||
public readonly ulong Sender;
|
||||
|
||||
public NetworkMessage(NetworkMessageType type, byte[] payload, ulong sender)
|
||||
{
|
||||
Type = type;
|
||||
Payload = payload;
|
||||
Sender = sender;
|
||||
}
|
||||
}
|
||||
|
||||
public readonly struct PlayerInputMessage
|
||||
{
|
||||
public readonly int NetworkId;
|
||||
public readonly Vector2 MoveInput;
|
||||
public readonly bool JumpPressed;
|
||||
public readonly Vector2 LookDelta;
|
||||
|
||||
public PlayerInputMessage(int networkId, Vector2 moveInput, bool jumpPressed, Vector2 lookDelta)
|
||||
{
|
||||
NetworkId = networkId;
|
||||
MoveInput = moveInput;
|
||||
JumpPressed = jumpPressed;
|
||||
LookDelta = lookDelta;
|
||||
}
|
||||
|
||||
public static byte[] Serialize(PlayerInputMessage message)
|
||||
{
|
||||
using var writer = new NetworkWriter();
|
||||
writer.Write(message.NetworkId);
|
||||
writer.Write(message.MoveInput.x);
|
||||
writer.Write(message.MoveInput.y);
|
||||
writer.Write(message.JumpPressed);
|
||||
writer.Write(message.LookDelta.x);
|
||||
writer.Write(message.LookDelta.y);
|
||||
return writer.ToArray();
|
||||
}
|
||||
|
||||
public static PlayerInputMessage Deserialize(byte[] buffer)
|
||||
{
|
||||
using var reader = new NetworkReader(buffer);
|
||||
int id = reader.ReadInt();
|
||||
float moveX = reader.ReadFloat();
|
||||
float moveY = reader.ReadFloat();
|
||||
bool jump = reader.ReadBool();
|
||||
float lookX = reader.ReadFloat();
|
||||
float lookY = reader.ReadFloat();
|
||||
return new PlayerInputMessage(id, new Vector2(moveX, moveY), jump, new Vector2(lookX, lookY));
|
||||
}
|
||||
}
|
||||
|
||||
public readonly struct CharacterTransformMessage
|
||||
{
|
||||
public readonly int NetworkId;
|
||||
public readonly Vector3 Position;
|
||||
public readonly Quaternion Rotation;
|
||||
public readonly Vector3 Velocity;
|
||||
|
||||
public CharacterTransformMessage(int networkId, Vector3 position, Quaternion rotation, Vector3 velocity)
|
||||
{
|
||||
NetworkId = networkId;
|
||||
Position = position;
|
||||
Rotation = rotation;
|
||||
Velocity = velocity;
|
||||
}
|
||||
|
||||
public static byte[] Serialize(CharacterTransformMessage message)
|
||||
{
|
||||
using var writer = new NetworkWriter();
|
||||
writer.Write(message.NetworkId);
|
||||
writer.Write(message.Position);
|
||||
writer.Write(message.Rotation);
|
||||
writer.Write(message.Velocity);
|
||||
return writer.ToArray();
|
||||
}
|
||||
|
||||
public static CharacterTransformMessage Deserialize(byte[] buffer)
|
||||
{
|
||||
using var reader = new NetworkReader(buffer);
|
||||
int id = reader.ReadInt();
|
||||
Vector3 position = reader.ReadVector3();
|
||||
Quaternion rotation = reader.ReadQuaternion();
|
||||
Vector3 velocity = reader.ReadVector3();
|
||||
return new CharacterTransformMessage(id, position, rotation, velocity);
|
||||
}
|
||||
}
|
||||
|
||||
public readonly struct WeaponFireMessage
|
||||
{
|
||||
public readonly int NetworkId;
|
||||
public readonly int WeaponIndex;
|
||||
public readonly Vector3 MuzzlePosition;
|
||||
public readonly Vector3 Direction;
|
||||
public readonly float Timestamp;
|
||||
|
||||
public WeaponFireMessage(int networkId, int weaponIndex, Vector3 muzzlePosition, Vector3 direction, float timestamp)
|
||||
{
|
||||
NetworkId = networkId;
|
||||
WeaponIndex = weaponIndex;
|
||||
MuzzlePosition = muzzlePosition;
|
||||
Direction = direction;
|
||||
Timestamp = timestamp;
|
||||
}
|
||||
|
||||
public static byte[] Serialize(WeaponFireMessage message)
|
||||
{
|
||||
using var writer = new NetworkWriter();
|
||||
writer.Write(message.NetworkId);
|
||||
writer.Write(message.WeaponIndex);
|
||||
writer.Write(message.MuzzlePosition);
|
||||
writer.Write(message.Direction);
|
||||
writer.Write(message.Timestamp);
|
||||
return writer.ToArray();
|
||||
}
|
||||
|
||||
public static WeaponFireMessage Deserialize(byte[] buffer)
|
||||
{
|
||||
using var reader = new NetworkReader(buffer);
|
||||
int networkId = reader.ReadInt();
|
||||
int index = reader.ReadInt();
|
||||
Vector3 muzzle = reader.ReadVector3();
|
||||
Vector3 direction = reader.ReadVector3();
|
||||
float time = reader.ReadFloat();
|
||||
return new WeaponFireMessage(networkId, index, muzzle, direction, time);
|
||||
}
|
||||
}
|
||||
|
||||
public readonly struct HealthSyncMessage
|
||||
{
|
||||
public readonly int NetworkId;
|
||||
public readonly float NormalizedHealth;
|
||||
|
||||
public HealthSyncMessage(int networkId, float normalizedHealth)
|
||||
{
|
||||
NetworkId = networkId;
|
||||
NormalizedHealth = normalizedHealth;
|
||||
}
|
||||
|
||||
public static byte[] Serialize(HealthSyncMessage message)
|
||||
{
|
||||
using var writer = new NetworkWriter();
|
||||
writer.Write(message.NetworkId);
|
||||
writer.Write(message.NormalizedHealth);
|
||||
return writer.ToArray();
|
||||
}
|
||||
|
||||
public static HealthSyncMessage Deserialize(byte[] buffer)
|
||||
{
|
||||
using var reader = new NetworkReader(buffer);
|
||||
int id = reader.ReadInt();
|
||||
float normalized = reader.ReadFloat();
|
||||
return new HealthSyncMessage(id, normalized);
|
||||
}
|
||||
}
|
||||
|
||||
public readonly struct ProjectileSpawnMessage
|
||||
{
|
||||
public readonly int NetworkId;
|
||||
public readonly int WeaponIndex;
|
||||
public readonly Vector3 Position;
|
||||
public readonly Vector3 Direction;
|
||||
public readonly float Speed;
|
||||
public readonly float Life;
|
||||
public readonly float Damage;
|
||||
|
||||
public ProjectileSpawnMessage(int networkId, int weaponIndex, Vector3 position, Vector3 direction, float speed, float life, float damage)
|
||||
{
|
||||
NetworkId = networkId;
|
||||
WeaponIndex = weaponIndex;
|
||||
Position = position;
|
||||
Direction = direction;
|
||||
Speed = speed;
|
||||
Life = life;
|
||||
Damage = damage;
|
||||
}
|
||||
|
||||
public static byte[] Serialize(ProjectileSpawnMessage message)
|
||||
{
|
||||
using var writer = new NetworkWriter();
|
||||
writer.Write(message.NetworkId);
|
||||
writer.Write(message.WeaponIndex);
|
||||
writer.Write(message.Position);
|
||||
writer.Write(message.Direction);
|
||||
writer.Write(message.Speed);
|
||||
writer.Write(message.Life);
|
||||
writer.Write(message.Damage);
|
||||
return writer.ToArray();
|
||||
}
|
||||
|
||||
public static ProjectileSpawnMessage Deserialize(byte[] buffer)
|
||||
{
|
||||
using var reader = new NetworkReader(buffer);
|
||||
int networkId = reader.ReadInt();
|
||||
int index = reader.ReadInt();
|
||||
Vector3 position = reader.ReadVector3();
|
||||
Vector3 direction = reader.ReadVector3();
|
||||
float speed = reader.ReadFloat();
|
||||
float life = reader.ReadFloat();
|
||||
float damage = reader.ReadFloat();
|
||||
return new ProjectileSpawnMessage(networkId, index, position, direction, speed, life, damage);
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Game/Scripts/Networking/NetworkMessages.cs.meta
Normal file
2
Game/Scripts/Networking/NetworkMessages.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 28ade96a26da5d9f693df8a5a80b0970
|
||||
116
Game/Scripts/Networking/NetworkSerialization.cs
Normal file
116
Game/Scripts/Networking/NetworkSerialization.cs
Normal file
@@ -0,0 +1,116 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using UnityEngine;
|
||||
|
||||
namespace MegaKoop.Game.Networking
|
||||
{
|
||||
public sealed class NetworkWriter : IDisposable
|
||||
{
|
||||
private readonly MemoryStream stream;
|
||||
private readonly BinaryWriter writer;
|
||||
|
||||
public NetworkWriter(int capacity = 128)
|
||||
{
|
||||
stream = new MemoryStream(capacity);
|
||||
writer = new BinaryWriter(stream);
|
||||
}
|
||||
|
||||
public void Write(byte value) => writer.Write(value);
|
||||
public void Write(int value) => writer.Write(value);
|
||||
public void Write(uint value) => writer.Write(value);
|
||||
public void Write(short value) => writer.Write(value);
|
||||
public void Write(ushort value) => writer.Write(value);
|
||||
public void Write(long value) => writer.Write(value);
|
||||
public void Write(ulong value) => writer.Write(value);
|
||||
public void Write(float value) => writer.Write(value);
|
||||
public void Write(bool value) => writer.Write(value);
|
||||
|
||||
public void Write(Vector3 value)
|
||||
{
|
||||
writer.Write(value.x);
|
||||
writer.Write(value.y);
|
||||
writer.Write(value.z);
|
||||
}
|
||||
|
||||
public void Write(Quaternion value)
|
||||
{
|
||||
writer.Write(value.x);
|
||||
writer.Write(value.y);
|
||||
writer.Write(value.z);
|
||||
writer.Write(value.w);
|
||||
}
|
||||
|
||||
public void Write(string value) => writer.Write(value ?? string.Empty);
|
||||
|
||||
public byte[] ToArray()
|
||||
{
|
||||
writer.Flush();
|
||||
return stream.ToArray();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
writer?.Dispose();
|
||||
stream?.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class NetworkReader : IDisposable
|
||||
{
|
||||
private readonly MemoryStream stream;
|
||||
private readonly BinaryReader reader;
|
||||
|
||||
public NetworkReader(byte[] buffer)
|
||||
{
|
||||
stream = new MemoryStream(buffer ?? Array.Empty<byte>());
|
||||
reader = new BinaryReader(stream);
|
||||
}
|
||||
|
||||
public byte ReadByte() => reader.ReadByte();
|
||||
public bool ReadBool() => reader.ReadBoolean();
|
||||
public int ReadInt() => reader.ReadInt32();
|
||||
public uint ReadUInt() => reader.ReadUInt32();
|
||||
public short ReadShort() => reader.ReadInt16();
|
||||
public ushort ReadUShort() => reader.ReadUInt16();
|
||||
public long ReadLong() => reader.ReadInt64();
|
||||
public ulong ReadULong() => reader.ReadUInt64();
|
||||
public float ReadFloat() => reader.ReadSingle();
|
||||
|
||||
public Vector3 ReadVector3()
|
||||
{
|
||||
float x = reader.ReadSingle();
|
||||
float y = reader.ReadSingle();
|
||||
float z = reader.ReadSingle();
|
||||
return new Vector3(x, y, z);
|
||||
}
|
||||
|
||||
public Quaternion ReadQuaternion()
|
||||
{
|
||||
float x = reader.ReadSingle();
|
||||
float y = reader.ReadSingle();
|
||||
float z = reader.ReadSingle();
|
||||
float w = reader.ReadSingle();
|
||||
return new Quaternion(x, y, z, w);
|
||||
}
|
||||
|
||||
public string ReadString() => reader.ReadString();
|
||||
|
||||
public bool TryReadBytes(int length, out byte[] bytes)
|
||||
{
|
||||
if (length <= 0 || reader.BaseStream.Position + length > reader.BaseStream.Length)
|
||||
{
|
||||
bytes = Array.Empty<byte>();
|
||||
return false;
|
||||
}
|
||||
|
||||
bytes = reader.ReadBytes(length);
|
||||
return true;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
reader?.Dispose();
|
||||
stream?.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Game/Scripts/Networking/NetworkSerialization.cs.meta
Normal file
2
Game/Scripts/Networking/NetworkSerialization.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0303a875e193063078b4557274bb3c5a
|
||||
101
Game/Scripts/Networking/SteamBootstrap.cs
Normal file
101
Game/Scripts/Networking/SteamBootstrap.cs
Normal file
@@ -0,0 +1,101 @@
|
||||
using Steamworks;
|
||||
using UnityEngine;
|
||||
|
||||
namespace MegaKoop.Game.Networking
|
||||
{
|
||||
/// <summary>
|
||||
/// Handles SteamAPI initialization and callback pumping. Place once in the initial scene.
|
||||
/// </summary>
|
||||
[DefaultExecutionOrder(-1000)]
|
||||
public class SteamBootstrap : MonoBehaviour
|
||||
{
|
||||
private static bool isInitialized;
|
||||
private static SteamBootstrap instance;
|
||||
|
||||
public static bool IsInitialized => isInitialized;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
if (instance != null)
|
||||
{
|
||||
Destroy(gameObject);
|
||||
return;
|
||||
}
|
||||
|
||||
instance = this;
|
||||
DontDestroyOnLoad(gameObject);
|
||||
|
||||
TryInitializeSteam();
|
||||
}
|
||||
|
||||
private void TryInitializeSteam()
|
||||
{
|
||||
if (isInitialized)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Packsize.Test())
|
||||
{
|
||||
Debug.LogError("[SteamBootstrap] Packsize Test returned false. Wrong Steamworks binaries for this platform?");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!DllCheck.Test())
|
||||
{
|
||||
Debug.LogError("[SteamBootstrap] DllCheck Test returned false. Missing Steamworks dependencies.");
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
isInitialized = SteamAPI.Init();
|
||||
}
|
||||
catch (System.DllNotFoundException e)
|
||||
{
|
||||
Debug.LogError("[SteamBootstrap] Steamworks native binaries not found: " + e.Message);
|
||||
isInitialized = false;
|
||||
}
|
||||
|
||||
if (!isInitialized)
|
||||
{
|
||||
Debug.LogError("[SteamBootstrap] Failed to initialize Steam API.");
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.Log("[SteamBootstrap] Steam API initialized for user " + SteamFriends.GetPersonaName());
|
||||
}
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (!isInitialized)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
SteamAPI.RunCallbacks();
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
if (instance == this)
|
||||
{
|
||||
ShutdownSteam();
|
||||
instance = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void ShutdownSteam()
|
||||
{
|
||||
if (!isInitialized)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
SteamAPI.Shutdown();
|
||||
isInitialized = false;
|
||||
Debug.Log("[SteamBootstrap] Steam API shutdown.");
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Game/Scripts/Networking/SteamBootstrap.cs.meta
Normal file
2
Game/Scripts/Networking/SteamBootstrap.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f741ec3a1d16461a98d02ec211293449
|
||||
261
Game/Scripts/Networking/SteamCharacterNetworkBridge.cs
Normal file
261
Game/Scripts/Networking/SteamCharacterNetworkBridge.cs
Normal file
@@ -0,0 +1,261 @@
|
||||
using Steamworks;
|
||||
using UnityEngine;
|
||||
|
||||
namespace MegaKoop.Game.Networking
|
||||
{
|
||||
[DisallowMultipleComponent]
|
||||
public class SteamCharacterNetworkBridge : MonoBehaviour
|
||||
{
|
||||
[Header("References")]
|
||||
[SerializeField] private ThirdPersonCharacterController characterController;
|
||||
[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;
|
||||
|
||||
public void AssignOwner(ulong steamId, bool localPlayer)
|
||||
{
|
||||
ownerSteamId = steamId;
|
||||
isLocalPlayer = localPlayer;
|
||||
localOverrideSet = true;
|
||||
UpdateAuthority();
|
||||
ConfigureController();
|
||||
}
|
||||
|
||||
public bool IsLocalPlayer => isLocalPlayer;
|
||||
public bool IsAuthority => isAuthority;
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
remoteTargetPosition = rootTransform.position;
|
||||
remoteTargetRotation = rootTransform.rotation;
|
||||
}
|
||||
|
||||
private void Start()
|
||||
{
|
||||
networkManager = SteamCoopNetworkManager.Instance;
|
||||
UpdateAuthority();
|
||||
ConfigureController();
|
||||
}
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
networkManager = SteamCoopNetworkManager.Instance;
|
||||
RegisterHandlers();
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
UnregisterHandlers();
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
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);
|
||||
isRegistered = true;
|
||||
}
|
||||
|
||||
private void UnregisterHandlers()
|
||||
{
|
||||
if (networkManager == null || !isRegistered)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
networkManager.UnregisterHandler(NetworkMessageType.PlayerInput, HandlePlayerInputMessage);
|
||||
networkManager.UnregisterHandler(NetworkMessageType.CharacterTransform, HandleCharacterTransformMessage);
|
||||
isRegistered = false;
|
||||
}
|
||||
|
||||
private void UpdateAuthority()
|
||||
{
|
||||
if (networkManager == null)
|
||||
{
|
||||
networkManager = SteamCoopNetworkManager.Instance;
|
||||
}
|
||||
|
||||
bool isHost = networkManager != null && networkManager.IsHost;
|
||||
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 = isHost; // Host drives authoritative simulation.
|
||||
}
|
||||
|
||||
private void ConfigureController()
|
||||
{
|
||||
if (characterController == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (isAuthority)
|
||||
{
|
||||
characterController.enabled = true;
|
||||
characterController.SetInputSource(isLocalPlayer ? null : networkInputProxy);
|
||||
}
|
||||
else
|
||||
{
|
||||
characterController.enabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void BroadcastTransform()
|
||||
{
|
||||
if (identity == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var unityController = GetComponent<CharacterController>();
|
||||
Vector3 velocity = unityController != null ? unityController.velocity : Vector3.zero;
|
||||
var message = new CharacterTransformMessage(identity.NetworkId, rootTransform.position, rootTransform.rotation, velocity);
|
||||
byte[] payload = CharacterTransformMessage.Serialize(message);
|
||||
networkManager.SendToAll(NetworkMessageType.CharacterTransform, payload, 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 (transformMessage.NetworkId != identity.NetworkId)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
remoteTargetPosition = transformMessage.Position;
|
||||
remoteTargetRotation = transformMessage.Rotation;
|
||||
remoteTargetVelocity = transformMessage.Velocity;
|
||||
haveRemoteState = true;
|
||||
}
|
||||
|
||||
public void SendLocalInput(Vector2 moveInput, bool jump)
|
||||
{
|
||||
if (networkManager == null || identity == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var message = new PlayerInputMessage(identity.NetworkId, moveInput, jump, Vector2.zero);
|
||||
byte[] payload = PlayerInputMessage.Serialize(message);
|
||||
if (!networkManager.IsConnected || networkManager.IsHost)
|
||||
{
|
||||
// If we are host, feed directly.
|
||||
HandlePlayerInputMessage(new NetworkMessage(NetworkMessageType.PlayerInput, payload, SteamUser.GetSteamID().m_SteamID));
|
||||
}
|
||||
else
|
||||
{
|
||||
CSteamID lobby = networkManager.ActiveLobby;
|
||||
CSteamID lobbyOwner = lobby != CSteamID.Nil ? SteamMatchmaking.GetLobbyOwner(lobby) : CSteamID.Nil;
|
||||
if (lobbyOwner == CSteamID.Nil)
|
||||
{
|
||||
lobbyOwner = SteamUser.GetSteamID();
|
||||
}
|
||||
|
||||
networkManager.SendToPlayer(lobbyOwner, NetworkMessageType.PlayerInput, payload, EP2PSend.k_EP2PSendUnreliableNoDelay);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a010df74dca8515c3af51a9fc598af17
|
||||
165
Game/Scripts/Networking/SteamCoopNetworkManager.cs
Normal file
165
Game/Scripts/Networking/SteamCoopNetworkManager.cs
Normal file
@@ -0,0 +1,165 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Steamworks;
|
||||
using UnityEngine;
|
||||
|
||||
namespace MegaKoop.Game.Networking
|
||||
{
|
||||
/// <summary>
|
||||
/// High level orchestrator for Steam lobby + P2P messaging. Keeps track of handlers per message type.
|
||||
/// </summary>
|
||||
[DisallowMultipleComponent]
|
||||
public class SteamCoopNetworkManager : MonoBehaviour
|
||||
{
|
||||
public static SteamCoopNetworkManager Instance { get; private set; }
|
||||
|
||||
[SerializeField] private SteamLobbyManager lobbyManager;
|
||||
[SerializeField] private SteamP2PTransport p2pTransport;
|
||||
|
||||
private readonly Dictionary<NetworkMessageType, Action<NetworkMessage>> handlers = new();
|
||||
private bool isHost;
|
||||
private bool isConnected;
|
||||
|
||||
public bool IsHost => isHost;
|
||||
public bool IsConnected => isConnected;
|
||||
public CSteamID ActiveLobby => lobbyManager != null ? lobbyManager.GetActiveLobby() : CSteamID.Nil;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
if (Instance != null)
|
||||
{
|
||||
Destroy(gameObject);
|
||||
return;
|
||||
}
|
||||
|
||||
Instance = this;
|
||||
DontDestroyOnLoad(gameObject);
|
||||
|
||||
if (lobbyManager == null)
|
||||
{
|
||||
lobbyManager = GetComponentInChildren<SteamLobbyManager>();
|
||||
}
|
||||
|
||||
if (p2pTransport == null)
|
||||
{
|
||||
p2pTransport = GetComponentInChildren<SteamP2PTransport>();
|
||||
}
|
||||
|
||||
if (lobbyManager != null)
|
||||
{
|
||||
lobbyManager.LobbyCreated += HandleLobbyCreated;
|
||||
lobbyManager.LobbyJoined += HandleLobbyJoined;
|
||||
lobbyManager.LobbyMemberJoined += HandleLobbyMemberJoined;
|
||||
lobbyManager.LobbyMemberLeft += HandleLobbyMemberLeft;
|
||||
}
|
||||
|
||||
if (p2pTransport != null)
|
||||
{
|
||||
p2pTransport.MessageReceived += DispatchMessage;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
if (Instance == this)
|
||||
{
|
||||
Instance = null;
|
||||
}
|
||||
|
||||
if (lobbyManager != null)
|
||||
{
|
||||
lobbyManager.LobbyCreated -= HandleLobbyCreated;
|
||||
lobbyManager.LobbyJoined -= HandleLobbyJoined;
|
||||
lobbyManager.LobbyMemberJoined -= HandleLobbyMemberJoined;
|
||||
lobbyManager.LobbyMemberLeft -= HandleLobbyMemberLeft;
|
||||
}
|
||||
|
||||
if (p2pTransport != null)
|
||||
{
|
||||
p2pTransport.MessageReceived -= DispatchMessage;
|
||||
}
|
||||
}
|
||||
|
||||
public void RegisterHandler(NetworkMessageType type, Action<NetworkMessage> handler)
|
||||
{
|
||||
if (handlers.TryGetValue(type, out Action<NetworkMessage> existing))
|
||||
{
|
||||
existing += handler;
|
||||
handlers[type] = existing;
|
||||
}
|
||||
else
|
||||
{
|
||||
handlers[type] = handler;
|
||||
}
|
||||
}
|
||||
|
||||
public void UnregisterHandler(NetworkMessageType type, Action<NetworkMessage> handler)
|
||||
{
|
||||
if (!handlers.TryGetValue(type, out Action<NetworkMessage> existing))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
existing -= handler;
|
||||
if (existing == null)
|
||||
{
|
||||
handlers.Remove(type);
|
||||
}
|
||||
else
|
||||
{
|
||||
handlers[type] = existing;
|
||||
}
|
||||
}
|
||||
|
||||
public void SendToAll(NetworkMessageType type, byte[] payload, EP2PSend sendType = EP2PSend.k_EP2PSendReliable)
|
||||
{
|
||||
p2pTransport?.Broadcast(type, payload, sendType);
|
||||
}
|
||||
|
||||
public void SendToPlayer(CSteamID target, NetworkMessageType type, byte[] payload, EP2PSend sendType = EP2PSend.k_EP2PSendReliable)
|
||||
{
|
||||
p2pTransport?.Send(target, type, payload, sendType);
|
||||
}
|
||||
|
||||
private void DispatchMessage(NetworkMessage message)
|
||||
{
|
||||
if (handlers.TryGetValue(message.Type, out Action<NetworkMessage> handler))
|
||||
{
|
||||
handler?.Invoke(message);
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleLobbyCreated(CSteamID lobbyId)
|
||||
{
|
||||
isHost = true;
|
||||
isConnected = true;
|
||||
p2pTransport?.SetActiveLobby(lobbyId);
|
||||
}
|
||||
|
||||
private void HandleLobbyJoined(CSteamID lobbyId)
|
||||
{
|
||||
isConnected = true;
|
||||
p2pTransport?.SetActiveLobby(lobbyId);
|
||||
|
||||
string ownerId = SteamMatchmaking.GetLobbyData(lobbyId, "owner");
|
||||
if (!string.IsNullOrEmpty(ownerId) && ulong.TryParse(ownerId, out ulong ownerSteamId))
|
||||
{
|
||||
isHost = ownerSteamId == SteamUser.GetSteamID().m_SteamID;
|
||||
}
|
||||
else
|
||||
{
|
||||
isHost = SteamMatchmaking.GetLobbyOwner(lobbyId) == SteamUser.GetSteamID();
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleLobbyMemberJoined(CSteamID member)
|
||||
{
|
||||
Debug.Log("[SteamCoopNetworkManager] Member joined: " + member);
|
||||
}
|
||||
|
||||
private void HandleLobbyMemberLeft(CSteamID member)
|
||||
{
|
||||
Debug.Log("[SteamCoopNetworkManager] Member left: " + member);
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Game/Scripts/Networking/SteamCoopNetworkManager.cs.meta
Normal file
2
Game/Scripts/Networking/SteamCoopNetworkManager.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 83320aed3c99a87b692932447a34631e
|
||||
89
Game/Scripts/Networking/SteamHealthNetworkBridge.cs
Normal file
89
Game/Scripts/Networking/SteamHealthNetworkBridge.cs
Normal file
@@ -0,0 +1,89 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace MegaKoop.Game.Networking
|
||||
{
|
||||
[DisallowMultipleComponent]
|
||||
public class SteamHealthNetworkBridge : MonoBehaviour
|
||||
{
|
||||
[SerializeField] private Combat.Health health;
|
||||
[SerializeField] private NetworkIdentity identity;
|
||||
|
||||
private SteamCoopNetworkManager networkManager;
|
||||
private bool isRegistered;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
if (health == null)
|
||||
{
|
||||
health = GetComponent<Combat.Health>();
|
||||
}
|
||||
|
||||
if (identity == null)
|
||||
{
|
||||
identity = GetComponent<NetworkIdentity>();
|
||||
}
|
||||
}
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
networkManager = SteamCoopNetworkManager.Instance;
|
||||
if (networkManager != null)
|
||||
{
|
||||
networkManager.RegisterHandler(NetworkMessageType.HealthSync, HandleHealthSync);
|
||||
isRegistered = true;
|
||||
}
|
||||
|
||||
if (health != null)
|
||||
{
|
||||
health.NormalizedHealthChanged += OnHealthChanged;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
if (isRegistered && networkManager != null)
|
||||
{
|
||||
networkManager.UnregisterHandler(NetworkMessageType.HealthSync, HandleHealthSync);
|
||||
isRegistered = false;
|
||||
}
|
||||
|
||||
if (health != null)
|
||||
{
|
||||
health.NormalizedHealthChanged -= OnHealthChanged;
|
||||
}
|
||||
}
|
||||
|
||||
private bool IsAuthority()
|
||||
{
|
||||
return networkManager == null || networkManager.IsHost;
|
||||
}
|
||||
|
||||
private void OnHealthChanged(float normalized)
|
||||
{
|
||||
if (!IsAuthority() || identity == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var message = new HealthSyncMessage(identity.NetworkId, normalized);
|
||||
byte[] payload = HealthSyncMessage.Serialize(message);
|
||||
networkManager.SendToAll(NetworkMessageType.HealthSync, payload, Steamworks.EP2PSend.k_EP2PSendReliable);
|
||||
}
|
||||
|
||||
private void HandleHealthSync(NetworkMessage message)
|
||||
{
|
||||
if (IsAuthority() || identity == null || health == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
HealthSyncMessage syncMessage = HealthSyncMessage.Deserialize(message.Payload);
|
||||
if (syncMessage.NetworkId != identity.NetworkId)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
health.ForceSetNormalizedHealth(syncMessage.NormalizedHealth);
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Game/Scripts/Networking/SteamHealthNetworkBridge.cs.meta
Normal file
2
Game/Scripts/Networking/SteamHealthNetworkBridge.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a55005e32d7c7fdd89d4c15872d0466d
|
||||
116
Game/Scripts/Networking/SteamLobbyManager.cs
Normal file
116
Game/Scripts/Networking/SteamLobbyManager.cs
Normal file
@@ -0,0 +1,116 @@
|
||||
using System;
|
||||
using Steamworks;
|
||||
using UnityEngine;
|
||||
|
||||
namespace MegaKoop.Game.Networking
|
||||
{
|
||||
/// <summary>
|
||||
/// Wraps Steam lobby creation and join logic for cooperative sessions.
|
||||
/// </summary>
|
||||
[DisallowMultipleComponent]
|
||||
public class SteamLobbyManager : MonoBehaviour
|
||||
{
|
||||
[SerializeField] private int maxPlayers = 4;
|
||||
|
||||
public event Action<LobbyDataUpdate_t> LobbyDataUpdated;
|
||||
public event Action<CSteamID> LobbyCreated;
|
||||
public event Action<CSteamID> LobbyJoined;
|
||||
public event Action<CSteamID> LobbyMemberJoined;
|
||||
public event Action<CSteamID> LobbyMemberLeft;
|
||||
|
||||
private Callback<LobbyCreated_t> lobbyCreatedCallback;
|
||||
private Callback<LobbyEnter_t> lobbyEnterCallback;
|
||||
private Callback<LobbyDataUpdate_t> lobbyDataUpdateCallback;
|
||||
private Callback<LobbyChatUpdate_t> lobbyChatUpdateCallback;
|
||||
|
||||
private CSteamID activeLobbyId;
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
lobbyCreatedCallback = Callback<LobbyCreated_t>.Create(OnLobbyCreated);
|
||||
lobbyEnterCallback = Callback<LobbyEnter_t>.Create(OnLobbyEntered);
|
||||
lobbyDataUpdateCallback = Callback<LobbyDataUpdate_t>.Create(OnLobbyDataUpdated);
|
||||
lobbyChatUpdateCallback = Callback<LobbyChatUpdate_t>.Create(OnLobbyChatUpdate);
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
lobbyCreatedCallback?.Dispose();
|
||||
lobbyEnterCallback?.Dispose();
|
||||
lobbyDataUpdateCallback?.Dispose();
|
||||
lobbyChatUpdateCallback?.Dispose();
|
||||
}
|
||||
|
||||
public void HostLobby(string lobbyName)
|
||||
{
|
||||
if (!SteamBootstrap.IsInitialized || !SteamAPI.IsSteamRunning())
|
||||
{
|
||||
Debug.LogWarning("[SteamLobbyManager] Steam is not initialized; cannot create lobby.");
|
||||
return;
|
||||
}
|
||||
|
||||
SteamMatchmaking.CreateLobby(ELobbyType.k_ELobbyTypePublic, Mathf.Max(2, maxPlayers));
|
||||
pendingLobbyName = lobbyName;
|
||||
}
|
||||
|
||||
public void JoinLobby(CSteamID lobbyId)
|
||||
{
|
||||
if (!SteamBootstrap.IsInitialized || !SteamAPI.IsSteamRunning())
|
||||
{
|
||||
Debug.LogWarning("[SteamLobbyManager] Steam not running; cannot join lobby.");
|
||||
return;
|
||||
}
|
||||
|
||||
SteamMatchmaking.JoinLobby(lobbyId);
|
||||
}
|
||||
|
||||
public CSteamID GetActiveLobby() => activeLobbyId;
|
||||
|
||||
private string pendingLobbyName;
|
||||
|
||||
private void OnLobbyCreated(LobbyCreated_t callback)
|
||||
{
|
||||
if (callback.m_eResult != EResult.k_EResultOK)
|
||||
{
|
||||
Debug.LogError("[SteamLobbyManager] Lobby creation failed: " + callback.m_eResult);
|
||||
return;
|
||||
}
|
||||
|
||||
activeLobbyId = new CSteamID(callback.m_ulSteamIDLobby);
|
||||
SteamMatchmaking.SetLobbyData(activeLobbyId, "name", string.IsNullOrEmpty(pendingLobbyName) ? "MegaKoop Lobby" : pendingLobbyName);
|
||||
SteamMatchmaking.SetLobbyData(activeLobbyId, "owner", SteamUser.GetSteamID().ToString());
|
||||
LobbyCreated?.Invoke(activeLobbyId);
|
||||
Debug.Log("[SteamLobbyManager] Lobby created " + activeLobbyId);
|
||||
}
|
||||
|
||||
private void OnLobbyEntered(LobbyEnter_t callback)
|
||||
{
|
||||
activeLobbyId = new CSteamID(callback.m_ulSteamIDLobby);
|
||||
LobbyJoined?.Invoke(activeLobbyId);
|
||||
Debug.Log("[SteamLobbyManager] Entered lobby " + activeLobbyId);
|
||||
}
|
||||
|
||||
private void OnLobbyDataUpdated(LobbyDataUpdate_t callback)
|
||||
{
|
||||
LobbyDataUpdated?.Invoke(callback);
|
||||
}
|
||||
|
||||
private void OnLobbyChatUpdate(LobbyChatUpdate_t callback)
|
||||
{
|
||||
CSteamID lobby = new CSteamID(callback.m_ulSteamIDLobby);
|
||||
CSteamID changedUser = new CSteamID(callback.m_ulSteamIDUserChanged);
|
||||
|
||||
EChatMemberStateChange stateChange = (EChatMemberStateChange)callback.m_rgfChatMemberStateChange;
|
||||
|
||||
if ((stateChange & EChatMemberStateChange.k_EChatMemberStateChangeEntered) != 0)
|
||||
{
|
||||
LobbyMemberJoined?.Invoke(changedUser);
|
||||
}
|
||||
|
||||
if ((stateChange & (EChatMemberStateChange.k_EChatMemberStateChangeLeft | EChatMemberStateChange.k_EChatMemberStateChangeDisconnected | EChatMemberStateChange.k_EChatMemberStateChangeKicked | EChatMemberStateChange.k_EChatMemberStateChangeBanned)) != 0)
|
||||
{
|
||||
LobbyMemberLeft?.Invoke(changedUser);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Game/Scripts/Networking/SteamLobbyManager.cs.meta
Normal file
2
Game/Scripts/Networking/SteamLobbyManager.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 468b0a865fb2a0b0181db1c60d4e0ea9
|
||||
44
Game/Scripts/Networking/SteamLocalInputSender.cs
Normal file
44
Game/Scripts/Networking/SteamLocalInputSender.cs
Normal file
@@ -0,0 +1,44 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace MegaKoop.Game.Networking
|
||||
{
|
||||
[DisallowMultipleComponent]
|
||||
public class SteamLocalInputSender : MonoBehaviour
|
||||
{
|
||||
[SerializeField] private SteamCharacterNetworkBridge characterNetwork;
|
||||
[SerializeField] private float sendInterval = 0.05f;
|
||||
|
||||
private float sendTimer;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
if (characterNetwork == null)
|
||||
{
|
||||
characterNetwork = GetComponent<SteamCharacterNetworkBridge>();
|
||||
}
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (characterNetwork == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!characterNetwork.IsLocalPlayer || characterNetwork.IsAuthority)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
sendTimer -= Time.deltaTime;
|
||||
Vector2 moveInput = new Vector2(Input.GetAxisRaw("Horizontal"), Input.GetAxisRaw("Vertical"));
|
||||
bool jumpPressed = Input.GetButtonDown("Jump");
|
||||
|
||||
if (sendTimer <= 0f || jumpPressed)
|
||||
{
|
||||
characterNetwork.SendLocalInput(moveInput, jumpPressed);
|
||||
sendTimer = sendInterval;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Game/Scripts/Networking/SteamLocalInputSender.cs.meta
Normal file
2
Game/Scripts/Networking/SteamLocalInputSender.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 07c0ee7fbf4c1edaf9992cf139839928
|
||||
114
Game/Scripts/Networking/SteamP2PTransport.cs
Normal file
114
Game/Scripts/Networking/SteamP2PTransport.cs
Normal file
@@ -0,0 +1,114 @@
|
||||
using System;
|
||||
using Steamworks;
|
||||
using UnityEngine;
|
||||
|
||||
namespace MegaKoop.Game.Networking
|
||||
{
|
||||
/// <summary>
|
||||
/// Low level Steam P2P messaging helper. Prepends a byte message type header for easy routing.
|
||||
/// </summary>
|
||||
public class SteamP2PTransport : MonoBehaviour
|
||||
{
|
||||
public event Action<NetworkMessage> MessageReceived;
|
||||
|
||||
[SerializeField] private EP2PSend defaultSendType = EP2PSend.k_EP2PSendReliable;
|
||||
[SerializeField] private int listenChannel = 0;
|
||||
|
||||
private byte[] receiveBuffer = new byte[8192];
|
||||
private CSteamID activeLobby = CSteamID.Nil;
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (!SteamBootstrap.IsInitialized)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
PumpIncomingPackets();
|
||||
}
|
||||
|
||||
public void SetActiveLobby(CSteamID lobbyId)
|
||||
{
|
||||
activeLobby = lobbyId;
|
||||
}
|
||||
|
||||
public void Send(CSteamID recipient, NetworkMessageType type, byte[] payload, EP2PSend sendType = EP2PSend.k_EP2PSendReliable)
|
||||
{
|
||||
if (!SteamBootstrap.IsInitialized)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
int payloadLength = payload != null ? payload.Length : 0;
|
||||
byte[] packet = new byte[payloadLength + 1];
|
||||
packet[0] = (byte)type;
|
||||
if (payloadLength > 0)
|
||||
{
|
||||
Buffer.BlockCopy(payload, 0, packet, 1, payloadLength);
|
||||
}
|
||||
|
||||
bool sent = SteamNetworking.SendP2PPacket(recipient, packet, (uint)packet.Length, sendType, listenChannel);
|
||||
if (!sent)
|
||||
{
|
||||
Debug.LogWarning("[SteamP2PTransport] Failed to send packet to " + recipient);
|
||||
}
|
||||
}
|
||||
|
||||
public void Broadcast(NetworkMessageType type, byte[] payload, EP2PSend sendType = EP2PSend.k_EP2PSendReliable)
|
||||
{
|
||||
if (activeLobby == CSteamID.Nil)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
int members = SteamMatchmaking.GetNumLobbyMembers(activeLobby);
|
||||
CSteamID self = SteamUser.GetSteamID();
|
||||
|
||||
for (int i = 0; i < members; i++)
|
||||
{
|
||||
CSteamID member = SteamMatchmaking.GetLobbyMemberByIndex(activeLobby, i);
|
||||
if (member == self)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
Send(member, type, payload, sendType);
|
||||
}
|
||||
}
|
||||
|
||||
private void PumpIncomingPackets()
|
||||
{
|
||||
while (SteamNetworking.IsP2PPacketAvailable(out uint packetSize, listenChannel))
|
||||
{
|
||||
if (packetSize == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (packetSize > receiveBuffer.Length)
|
||||
{
|
||||
receiveBuffer = new byte[(int)packetSize];
|
||||
}
|
||||
|
||||
if (SteamNetworking.ReadP2PPacket(receiveBuffer, (uint)receiveBuffer.Length, out uint bytesRead, out CSteamID remote, listenChannel))
|
||||
{
|
||||
SteamNetworking.AcceptP2PSessionWithUser(remote);
|
||||
|
||||
if (bytesRead == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
NetworkMessageType type = (NetworkMessageType)receiveBuffer[0];
|
||||
byte[] payload = new byte[Mathf.Max(0, (int)bytesRead - 1)];
|
||||
if (bytesRead > 1)
|
||||
{
|
||||
Buffer.BlockCopy(receiveBuffer, 1, payload, 0, (int)bytesRead - 1);
|
||||
}
|
||||
|
||||
MessageReceived?.Invoke(new NetworkMessage(type, payload, remote.m_SteamID));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Game/Scripts/Networking/SteamP2PTransport.cs.meta
Normal file
2
Game/Scripts/Networking/SteamP2PTransport.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c3923950963f6f6a98a81fda48267e6e
|
||||
128
Game/Scripts/Networking/SteamWeaponNetworkBridge.cs
Normal file
128
Game/Scripts/Networking/SteamWeaponNetworkBridge.cs
Normal file
@@ -0,0 +1,128 @@
|
||||
using MegaKoop.Game.WeaponSystem;
|
||||
using Steamworks;
|
||||
using UnityEngine;
|
||||
|
||||
namespace MegaKoop.Game.Networking
|
||||
{
|
||||
[DisallowMultipleComponent]
|
||||
public class SteamWeaponNetworkBridge : MonoBehaviour
|
||||
{
|
||||
[SerializeField] private WeaponController weaponController;
|
||||
[SerializeField] private NetworkIdentity identity;
|
||||
[SerializeField] private bool disableLocalFiringWhenClient = true;
|
||||
|
||||
private SteamCoopNetworkManager networkManager;
|
||||
private bool isRegistered;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
networkManager = SteamCoopNetworkManager.Instance;
|
||||
|
||||
if (weaponController == null)
|
||||
{
|
||||
weaponController = GetComponent<WeaponController>();
|
||||
}
|
||||
|
||||
if (identity == null)
|
||||
{
|
||||
identity = GetComponent<NetworkIdentity>();
|
||||
}
|
||||
}
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
if (weaponController == null || identity == null)
|
||||
{
|
||||
Debug.LogWarning("[SteamWeaponNetworkBridge] Missing references.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (networkManager == null)
|
||||
{
|
||||
networkManager = SteamCoopNetworkManager.Instance;
|
||||
}
|
||||
|
||||
if (networkManager != null)
|
||||
{
|
||||
networkManager.RegisterHandler(NetworkMessageType.ProjectileSpawn, HandleProjectileSpawnMessage);
|
||||
isRegistered = true;
|
||||
}
|
||||
|
||||
weaponController.ProjectileSpawned += OnProjectileSpawned;
|
||||
|
||||
if (!IsAuthoritative() && disableLocalFiringWhenClient)
|
||||
{
|
||||
weaponController.enabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
if (weaponController != null)
|
||||
{
|
||||
weaponController.ProjectileSpawned -= OnProjectileSpawned;
|
||||
|
||||
if (!IsAuthoritative() && disableLocalFiringWhenClient)
|
||||
{
|
||||
weaponController.enabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (isRegistered && networkManager != null)
|
||||
{
|
||||
networkManager.UnregisterHandler(NetworkMessageType.ProjectileSpawn, HandleProjectileSpawnMessage);
|
||||
isRegistered = false;
|
||||
}
|
||||
}
|
||||
|
||||
private bool IsAuthoritative()
|
||||
{
|
||||
if (networkManager == null)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return networkManager.IsHost;
|
||||
}
|
||||
|
||||
private void OnProjectileSpawned(WeaponController.ProjectileSpawnEvent spawnEvent)
|
||||
{
|
||||
if (!IsAuthoritative())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (networkManager == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var message = new ProjectileSpawnMessage(identity.NetworkId, spawnEvent.WeaponIndex, spawnEvent.Position, spawnEvent.Direction, spawnEvent.Speed, spawnEvent.Lifetime, spawnEvent.Damage);
|
||||
byte[] payload = ProjectileSpawnMessage.Serialize(message);
|
||||
networkManager.SendToAll(NetworkMessageType.ProjectileSpawn, payload, EP2PSend.k_EP2PSendUnreliableNoDelay);
|
||||
}
|
||||
|
||||
private void HandleProjectileSpawnMessage(NetworkMessage message)
|
||||
{
|
||||
if (message.Sender == SteamUser.GetSteamID().m_SteamID)
|
||||
{
|
||||
// Ignore our own echo.
|
||||
return;
|
||||
}
|
||||
|
||||
ProjectileSpawnMessage spawnMessage = ProjectileSpawnMessage.Deserialize(message.Payload);
|
||||
if (spawnMessage.NetworkId != identity.NetworkId)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (IsAuthoritative())
|
||||
{
|
||||
// Host already spawned real projectile, don't duplicate.
|
||||
return;
|
||||
}
|
||||
|
||||
weaponController.SpawnNetworkProjectile(spawnMessage.WeaponIndex, spawnMessage.Position, spawnMessage.Direction, spawnMessage.Speed, spawnMessage.Life, spawnMessage.Damage);
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Game/Scripts/Networking/SteamWeaponNetworkBridge.cs.meta
Normal file
2
Game/Scripts/Networking/SteamWeaponNetworkBridge.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: dad61aa7b24bddb6b9add5a461263779
|
||||
Reference in New Issue
Block a user