Characters

This commit is contained in:
2025-10-05 18:21:16 +02:00
parent b52b3aa830
commit 174a399ee7
77 changed files with 14406 additions and 0 deletions

View File

@@ -0,0 +1,10 @@
using UnityEngine;
namespace MegaKoop.Game.Networking
{
public interface ICharacterInputSource
{
Vector2 MoveInput { get; }
bool JumpPressed { get; }
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 403a3945388ae5082bf455368a91a800

View 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;
}
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: e3118c9c432a7acbd824645749251552

View 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);
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 1e6f99ca4475ead269829ba672213d5c

View 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);
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 28ade96a26da5d9f693df8a5a80b0970

View 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();
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 0303a875e193063078b4557274bb3c5a

View 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.");
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: f741ec3a1d16461a98d02ec211293449

View 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);
}
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: a010df74dca8515c3af51a9fc598af17

View 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);
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 83320aed3c99a87b692932447a34631e

View 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);
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: a55005e32d7c7fdd89d4c15872d0466d

View 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);
}
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 468b0a865fb2a0b0181db1c60d4e0ea9

View 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;
}
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 07c0ee7fbf4c1edaf9992cf139839928

View 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));
}
}
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: c3923950963f6f6a98a81fda48267e6e

View 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);
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: dad61aa7b24bddb6b9add5a461263779