multiplayer stuff

This commit is contained in:
2025-07-01 18:57:49 +02:00
parent 04675ce941
commit 315661ed8d
48 changed files with 5298 additions and 349 deletions

View File

@@ -0,0 +1,47 @@
using System.Collections.Generic;
using RebootKit.Engine.Main;
using Unity.Netcode;
using UnityEngine;
namespace RebootReality.jelycho.Beacons {
public class BaseManager : NetworkBehaviour {
[SerializeField] Beacon m_BeaconPrefab;
[SerializeField] float m_ConnectionRopeLength = 10.0f;
[SerializeField] float m_BeaconSpawnRadius = 15.0f;
readonly List<Beacon> m_Beacons = new List<Beacon>();
public IReadOnlyList<Beacon> Beacons => m_Beacons;
[ServerRpc(RequireOwnership = false)]
public void SpawnBeaconServerRpc(Vector3 position) {
if (!IsServer) {
Debug.LogWarning("Only the server can spawn beacons.");
return;
}
Beacon beacon = Instantiate(m_BeaconPrefab, position, Quaternion.identity);
beacon.NetworkObject.Spawn();
beacon.GrowClientRpc();
foreach (Beacon otherBeacon in m_Beacons) {
if ((otherBeacon.transform.position - beacon.transform.position).sqrMagnitude <
m_BeaconSpawnRadius * m_BeaconSpawnRadius) {
Vector3 startPosition = otherBeacon.RopeConnectionPoint.position;
Vector3 endPosition = beacon.RopeConnectionPoint.position;
SpawnRopeRpc(startPosition, endPosition);
}
}
m_Beacons.Add(beacon);
}
[Rpc(SendTo.Everyone)]
void SpawnRopeRpc(Vector3 startPosition, Vector3 endPosition) {
if (RR.World.Context is WorldContext worldContext) {
worldContext.RopesManager.SpawnLockedRope(m_ConnectionRopeLength, startPosition, endPosition);
}
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: ac767dd7ed4c424ebad060b7149afac8
timeCreated: 1751317007

View File

@@ -0,0 +1,14 @@
using Unity.Netcode;
using UnityEngine;
namespace RebootReality.jelycho.Beacons {
public class Beacon : NetworkBehaviour {
[SerializeField] BeaconGraphics m_Graphics;
[field: SerializeField] public Transform RopeConnectionPoint { get; private set; }
[ClientRpc]
public void GrowClientRpc() {
m_Graphics.Grow();
}
}
}

View File

@@ -1,10 +0,0 @@
using RebootKit.Engine.Services.Simulation;
using RebootKit.Engine.Simulation;
namespace RebootReality.jelycho.Beacons {
public class BeaconActor : Actor {
}
}

View File

@@ -0,0 +1,123 @@
using System;
using Unity.Mathematics;
using UnityEngine;
namespace RebootReality.jelycho.Beacons {
public class BeaconGraphics : MonoBehaviour {
[SerializeField] BeaconElement[] m_BeaconElements;
[Range(0.0f, 1.0f)] public float growAmount = 0.5f;
[SerializeField] public float growSpeed = 0.5f;
float m_CurrentGrowAmount = 0.0f;
void Update() {
if (m_CurrentGrowAmount >= 1.0f) {
foreach (BeaconElement beaconElement in m_BeaconElements) {
if (!beaconElement.usePulsing) {
continue;
}
float t = (Mathf.Sin(Time.time * beaconElement.pulsingSpeed) + 1.0f) * 0.5f;
float scale = Mathf.Lerp(beaconElement.pulsingMinScale,
beaconElement.pulsingMaxScale,
t);
beaconElement.transform.localScale = Vector3.one * scale;
}
}
if (Mathf.Approximately(m_CurrentGrowAmount, growAmount)) {
return;
}
m_CurrentGrowAmount = Mathf.MoveTowards(m_CurrentGrowAmount,
growAmount,
growSpeed * Time.deltaTime);
UpdateElements(m_CurrentGrowAmount);
}
void UpdateElements(float amount) {
foreach (BeaconElement element in m_BeaconElements) {
if (element.useLocalPosition && element.transform is not null) {
float localPositionT = math.remap(element.startLocalPositionAt,
element.endLocalPositionAt,
0.0f,
1.0f,
amount);
Vector3 localPosition =
Vector3.Lerp(element.minLocalPosition, element.maxLocalPosition, Mathf.Clamp01(localPositionT));
element.transform.localPosition = localPosition;
}
if (element.useScale && element.transform is not null) {
float scaleT = math.remap(element.startScaleAt,
element.endScaleAt,
0.0f,
1.0f,
amount);
Vector3 scale = Vector3.Lerp(element.minScale, element.maxScale, Mathf.Clamp01(scaleT));
element.transform.localScale = scale;
}
if (element.useCutoff && element.meshRenderer is not null) {
float cutoffT = math.remap(element.startCutoffAt,
element.endCutoffAt,
0.0f,
1.0f,
amount);
float cutoff = Mathf.Lerp(0.0f, 1.0f, Mathf.Clamp01(cutoffT));
if (element.flipCutoffDirection) {
cutoff = 1.0f - cutoff;
}
Material material = element.meshRenderer.materials[element.materialIndex];
material.SetFloat("_Cutoff", cutoff);
}
}
}
public void Grow() {
m_CurrentGrowAmount = 0.0f;
UpdateElements(m_CurrentGrowAmount);
growAmount = 1.0f;
}
[Serializable]
struct BeaconElement {
public Transform transform;
public MeshRenderer meshRenderer;
[Header("Local Position(Transform)")]
public bool useLocalPosition;
public Vector3 minLocalPosition;
public Vector3 maxLocalPosition;
public float startLocalPositionAt;
public float endLocalPositionAt;
[Header("Scale (Transform)")]
public bool useScale;
public Vector3 minScale;
public Vector3 maxScale;
public float startScaleAt;
public float endScaleAt;
[Header("Mesh Cutoff (MeshRenderer)")]
public bool useCutoff;
public bool flipCutoffDirection;
public float startCutoffAt;
public float endCutoffAt;
public int materialIndex;
[Header("Pulsing (Transform)")]
public bool usePulsing;
public float pulsingSpeed;
public float pulsingMinScale;
public float pulsingMaxScale;
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 7db2986869814a7495d4f30cbd35d713
timeCreated: 1751312364

View File

@@ -0,0 +1,35 @@
using System;
using RebootKit.Engine.Main;
using RebootReality.jelycho.Beacons;
using Unity.Netcode;
using UnityEngine;
using Logger = RebootKit.Engine.Foundation.Logger;
namespace RebootReality.jelycho.Main {
public class GameWorldController : NetworkWorldController {
static readonly Logger s_Logger = new Logger(nameof(GameWorldController));
public static GameWorldController Instance { get; private set; }
[SerializeField] BaseManager m_BaseManager;
void Awake() {
Instance = this;
}
public override void OnDestroy() {
Instance = null;
base.OnDestroy();
}
[Rpc(SendTo.Server)]
public void RequestBeaconSpawnRpc(Vector3 position) {
if (!IsServer) {
s_Logger.Error("Only the server can spawn beacons.");
return;
}
m_BaseManager.SpawnBeaconServerRpc(position);
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 3d6f280a434549d99d5c2b7f178ba6b6
timeCreated: 1751377186

View File

@@ -21,12 +21,17 @@ namespace RebootReality.jelycho.Main {
public class JelychoGame : Game {
static readonly Logger s_Logger = new Logger(nameof(JelychoGame));
[Header("Gameplay")]
[SerializeField] GameWorldController m_GameWorldControllerPrefab;
public GameWorldController m_GameWorldController;
[Header("Player")]
[SerializeField] PlayerController m_PlayerControllerPrefab;
[SerializeField] PlayerActor m_PlayerActorPrefab;
[SerializeField] string m_MainWorldID = "dev";
List<PlayerState> m_PlayerStates = new List<PlayerState>();
readonly List<PlayerState> m_PlayerStates = new List<PlayerState>();
void Awake() { }
@@ -81,9 +86,23 @@ namespace RebootReality.jelycho.Main {
public override void OnWorldLoaded() {
base.OnWorldLoaded();
if (IsServer) {
m_GameWorldController = Instantiate(m_GameWorldControllerPrefab);
m_GameWorldController.NetworkObject.Spawn();
}
OnPlayerReadyRpc(NetworkManager.Singleton.LocalClientId);
}
public override void OnWorldUnload() {
base.OnWorldUnload();
if (IsServer) {
m_GameWorldController.NetworkObject.Despawn();
Destroy(m_GameWorldController.gameObject);
}
}
[Rpc(SendTo.Server)]
void OnPlayerReadyRpc(ulong clientID) {
@@ -111,12 +130,6 @@ namespace RebootReality.jelycho.Main {
s_Logger.Error($"Player state for client {clientID} not found");
return null;
}
public override void OnWorldUnload() {
base.OnWorldUnload();
if (IsServer) { }
}
}
public static class JelychoConsoleCommands {
@@ -142,12 +155,16 @@ namespace RebootReality.jelycho.Main {
RR.WriteToConsole("Connecting to server...");
// if (args.Length != 2) {
// RR.Console.WriteToOutput($"Usage: {args[0]} <steam_id>");
// return;
// }
if (args.Length != 2) {
RR.WriteToConsole($"Usage: {args[0]} <steam_id>");
return;
}
RR.Connect();
if (ulong.TryParse(args[1], out ulong steamID)) {
RR.ConnectWithSteamID(steamID);
} else {
RR.WriteToConsole($"Invalid Steam ID: {args[1]}");
}
}
[RCCMD("disconnect", "Disconnects from the current server")]

View File

@@ -1,6 +1,7 @@
using System;
using RebootKit.Engine.Foundation;
using RebootKit.Engine.Services.Simulation.Sensors;
using RebootKit.Engine.Simulation.Sensors;
using RebootReality.jelycho.Main;
using Unity.Cinemachine;
using UnityEngine;

View File

@@ -2,7 +2,10 @@
using RebootKit.Engine.Foundation;
using RebootKit.Engine.Main;
using RebootKit.Engine.Services.Simulation;
using RebootKit.Engine.Services.Simulation.Sensors;
using RebootKit.Engine.Simulation;
using RebootKit.Engine.Simulation.Sensors;
using RebootReality.jelycho.Main;
using Unity.Collections;
using Unity.Mathematics;
using Unity.Netcode;
@@ -68,6 +71,11 @@ namespace RebootReality.jelycho.Player {
[SerializeField] PhysicsObjectDragger m_PhysicsDragger;
[SerializeField] FloatRange m_DragDistanceRange = new FloatRange(1.0f, 5.0f);
[Header("Beacon location picking")]
[SerializeField] LayerMask m_BeaconPlacementLayerMask = 0;
[SerializeField] float m_BeaconPlacementMaxDistance = 15.0f;
[SerializeField] float m_NormalDotUpThreshold = 0.5f;
[Header("Network")]
[SerializeField] float m_MinTeleportDistance = 0.5f;
@@ -302,13 +310,44 @@ namespace RebootReality.jelycho.Player {
}
public void PrimaryAction() {
m_Animator.SetTrigger(AnimatorParamHashes.Attack);
if (!IsOwner) {
s_Logger.Error("Only the owner can perform primary actions.");
return;
}
if (TryGetBeaconPosition(out Vector3 beaconPosition)) {
SetAnimatorTriggerRpc(AnimatorParamHashes.Throw);
GameWorldController.Instance.RequestBeaconSpawnRpc(beaconPosition);
}
}
public void SecondaryAction() {
if (!IsOwner) {
s_Logger.Error("Only the owner can perform secondary actions.");
return;
}
m_Animator.SetTrigger(AnimatorParamHashes.Block);
}
[Rpc(SendTo.Everyone)]
void SetAnimatorTriggerRpc(int hash) {
m_Animator.SetTrigger(hash);
}
bool TryGetBeaconPosition(out Vector3 position) {
Ray ray = new Ray(m_Camera.Camera.transform.position, m_Camera.Camera.transform.forward);
if (Physics.Raycast(ray, out RaycastHit hit, m_BeaconPlacementMaxDistance, m_BeaconPlacementLayerMask) &&
Vector3.Dot(hit.normal, Vector3.up) >= m_NormalDotUpThreshold) {
position = hit.point;
return true;
}
position = Vector3.zero;
return false;
}
struct AnimatorParamHashes {
public static readonly int VelocityForwardNormalized = Animator.StringToHash("VelocityForwardNormalized");
public static readonly int VelocityRightNormalized = Animator.StringToHash("VelocityRightNormalized");
@@ -317,6 +356,9 @@ namespace RebootReality.jelycho.Player {
public static readonly int Attack = Animator.StringToHash("Attack");
public static readonly int Block = Animator.StringToHash("Block");
public static readonly int Throw = Animator.StringToHash("Throw");
public static readonly int Holding = Animator.StringToHash("Holding");
}
void UpdateAnimator(Vector3 velocity) {
@@ -336,6 +378,7 @@ namespace RebootReality.jelycho.Player {
m_Animator.SetFloat(AnimatorParamHashes.TurnVelocity, turnVelocity);
m_Animator.SetBool(AnimatorParamHashes.IsGrounded, m_Locomotion.IsGrounded);
m_Animator.SetInteger(AnimatorParamHashes.Holding, 1);
}
}
}

View File

@@ -125,6 +125,21 @@ namespace RebootReality.jelycho.Ropes {
m_Ropes.Add(rope);
}
public void SpawnLockedRope(float ropeLength, float3 start, float3 end) {
int segmentsCount = (int)(ropeLength / m_RopeSegmentLength) + 1;
NativeArray<float3> positions = new NativeArray<float3>(segmentsCount, Allocator.Temp);
for (int i = 0; i < segmentsCount; ++i) {
float t = (float)i / (segmentsCount - 1);
positions[i] = math.lerp(start, end, t);
}
RopeData rope = new RopeData(positions);
rope.IsLocked[0] = true;
rope.IsLocked[rope.SegmentCount - 1] = true;
m_Ropes.Add(rope);
}
// @NOTE: Do not dispose the returned array, it is managed by the RopesManager.
public NativeArray<float3> PeekRopePositions(int index) {
return m_Ropes[index].Positions;

View File

@@ -1,5 +1,4 @@
using RebootKit.Engine.Services.Simulation;
using RebootKit.Engine.Simulation;
using RebootKit.Engine.Simulation;
using RebootReality.jelycho.Ropes;
using UnityEngine;