first commit
This commit is contained in:
378
NitroxClient/MonoBehaviours/Cyclops/CyclopsMotor.cs
Normal file
378
NitroxClient/MonoBehaviours/Cyclops/CyclopsMotor.cs
Normal file
@@ -0,0 +1,378 @@
|
||||
using NitroxClient.GameLogic;
|
||||
using NitroxClient.MonoBehaviours.Cyclops;
|
||||
using UnityEngine;
|
||||
using UnityEngine.XR;
|
||||
|
||||
namespace NitroxClient.MonoBehaviours;
|
||||
|
||||
/// <summary>
|
||||
/// A replacement for <see cref="GroundMotor"/> while Local Player is in a Cyclops.
|
||||
/// </summary>
|
||||
public partial class CyclopsMotor : GroundMotor
|
||||
{
|
||||
public GroundMotor ActualMotor { get; private set; }
|
||||
public CyclopsPawn Pawn;
|
||||
|
||||
private Transform body;
|
||||
private NitroxCyclops cyclops;
|
||||
private SubRoot sub;
|
||||
private Transform realAxis;
|
||||
private Transform virtualAxis;
|
||||
private WorldForces worldForces;
|
||||
|
||||
public Vector3 Up => virtualAxis.up;
|
||||
public float DeltaTime => Time.fixedDeltaTime;
|
||||
|
||||
private Vector3 verticalVelocity;
|
||||
private Vector3 latestVelocity;
|
||||
|
||||
public new void Awake()
|
||||
{
|
||||
controller = GetComponent<CharacterController>();
|
||||
controller.enabled = false;
|
||||
controller.stepOffset = controllerSetup.stepOffset;
|
||||
controller.slopeLimit = controllerSetup.slopeLimit;
|
||||
|
||||
rb = GetComponent<Rigidbody>();
|
||||
playerController = GetComponent<PlayerController>();
|
||||
worldForces = GetComponent<WorldForces>();
|
||||
|
||||
body = Player.mainObject.transform.Find("body");
|
||||
}
|
||||
|
||||
public override void SetEnabled(bool enabled)
|
||||
{
|
||||
base.SetEnabled(enabled);
|
||||
Setup(enabled);
|
||||
}
|
||||
|
||||
public void Initialize(GroundMotor reference)
|
||||
{
|
||||
ActualMotor = reference;
|
||||
movement = reference.movement;
|
||||
jumping = reference.jumping;
|
||||
movingPlatform = reference.movingPlatform;
|
||||
sliding = reference.sliding;
|
||||
controllerSetup = reference.controllerSetup;
|
||||
floatingModeSetup = reference.floatingModeSetup;
|
||||
allowMidAirJumping = reference.allowMidAirJumping;
|
||||
minWindSpeedToAffectMovement = reference.minWindSpeedToAffectMovement;
|
||||
percentWindDampeningOnGround = reference.percentWindDampeningOnGround;
|
||||
percentWindDampeningInAir = reference.percentWindDampeningInAir;
|
||||
floatingModeEnabled = reference.floatingModeEnabled;
|
||||
forwardMaxSpeed = reference.forwardMaxSpeed;
|
||||
backwardMaxSpeed = reference.backwardMaxSpeed;
|
||||
strafeMaxSpeed = reference.strafeMaxSpeed;
|
||||
verticalMaxSpeed = reference.verticalMaxSpeed;
|
||||
climbSpeed = reference.climbSpeed;
|
||||
gravity = reference.gravity;
|
||||
forwardSprintModifier = reference.forwardSprintModifier;
|
||||
strafeSprintModifier = reference.strafeSprintModifier;
|
||||
groundAcceleration = reference.groundAcceleration;
|
||||
airAcceleration = reference.airAcceleration;
|
||||
jumpHeight = reference.jumpHeight;
|
||||
SetEnabled(false);
|
||||
RecalculateConstants();
|
||||
}
|
||||
|
||||
public void SetCyclops(NitroxCyclops cyclops, SubRoot subRoot, CyclopsPawn pawn)
|
||||
{
|
||||
this.cyclops = cyclops;
|
||||
sub = subRoot;
|
||||
realAxis = sub.subAxis;
|
||||
virtualAxis = cyclops.Virtual.axis;
|
||||
Pawn = pawn;
|
||||
}
|
||||
|
||||
public void Setup(bool enabled)
|
||||
{
|
||||
verticalVelocity = Vector3.zero;
|
||||
latestVelocity = Vector3.zero;
|
||||
|
||||
if (enabled)
|
||||
{
|
||||
rb.isKinematic = false;
|
||||
Player.wantInterpolate = false;
|
||||
rb.detectCollisions = false;
|
||||
worldForces.lockInterpolation = true;
|
||||
worldForces.enabled = false;
|
||||
controller.detectCollisions = false;
|
||||
Player.mainCollider.isTrigger = true;
|
||||
UWE.Utils.EnterPhysicsSyncSection();
|
||||
}
|
||||
else
|
||||
{
|
||||
rb.isKinematic = true;
|
||||
Player.wantInterpolate = true;
|
||||
worldForces.lockInterpolation = false;
|
||||
rb.detectCollisions = true;
|
||||
worldForces.enabled = true;
|
||||
controller.detectCollisions = true;
|
||||
Player.mainCollider.isTrigger = false;
|
||||
UWE.Utils.ExitPhysicsSyncSection();
|
||||
}
|
||||
|
||||
Pawn?.SetReference();
|
||||
}
|
||||
|
||||
public override Vector3 UpdateMove()
|
||||
{
|
||||
if (!canControl)
|
||||
{
|
||||
return Vector3.zero;
|
||||
}
|
||||
|
||||
// Compute movements velocities based on inputs and previous movement
|
||||
Position = Pawn.Position;
|
||||
Center = cyclops.Virtual.transform.TransformVector(Pawn.Controller.center);
|
||||
Pawn.Handle.transform.localRotation = body.localRotation;
|
||||
|
||||
sprinting = false;
|
||||
verticalVelocity += CalculateVerticalVelocity();
|
||||
Vector3 horizontalVelocity = CalculateInputVelocity();
|
||||
|
||||
// movement.velocity gives velocity info for the animations and footsteps
|
||||
movement.velocity = Move(horizontalVelocity);
|
||||
return movement.velocity;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Simulates player movement on its pawn and update the grounded state
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Adapted from <see cref="GroundMotor.UpdateFunction"/>
|
||||
/// </remarks>
|
||||
/// <returns>Pawn's local velocity</returns>
|
||||
public Vector3 Move(Vector3 horizontalVelocity)
|
||||
{
|
||||
Vector3 beforePosition = Pawn.Position;
|
||||
|
||||
Vector3 velocity = new(horizontalVelocity.x, verticalVelocity.y, horizontalVelocity.z);
|
||||
Vector3 movementThisFrame = velocity * DeltaTime;
|
||||
|
||||
float step = Mathf.Max(Pawn.Controller.stepOffset, Mathf.Sqrt(movementThisFrame.x * movementThisFrame.x + movementThisFrame.z * movementThisFrame.z));
|
||||
if (grounded)
|
||||
{
|
||||
movementThisFrame -= step * Up;
|
||||
}
|
||||
|
||||
Collision = Pawn.Controller.Move(movementThisFrame);
|
||||
|
||||
float verticalDot = Vector3.Dot(verticalVelocity, Up);
|
||||
|
||||
bool previouslyGrounded = grounded;
|
||||
CheckGrounded(Collision, verticalDot <= 0f);
|
||||
|
||||
Vector3 velocityXZ = velocity._X0Z();
|
||||
Vector3 instantVelocity = (Pawn.Position - beforePosition) / DeltaTime;
|
||||
if (instantVelocity.sqrMagnitude <= 0.2f)
|
||||
{
|
||||
instantVelocity = velocity;
|
||||
}
|
||||
if (instantVelocity.y > 0f || Collision == CollisionFlags.None)
|
||||
{
|
||||
instantVelocity.y = velocity.y;
|
||||
}
|
||||
|
||||
latestVelocity = instantVelocity;
|
||||
|
||||
Vector3 instantVelocityXZ = instantVelocity._X0Z();
|
||||
if (velocityXZ == Vector3.zero)
|
||||
{
|
||||
latestVelocity = latestVelocity._0Y0();
|
||||
}
|
||||
else
|
||||
{
|
||||
float deviation = Vector3.Dot(instantVelocityXZ, velocityXZ) / velocityXZ.sqrMagnitude;
|
||||
latestVelocity = velocityXZ * Mathf.Clamp01(deviation) + latestVelocity.y * Up;
|
||||
}
|
||||
|
||||
if (latestVelocity.y < velocity.y - 0.001)
|
||||
{
|
||||
if (latestVelocity.y < 0f)
|
||||
{
|
||||
latestVelocity.y = velocity.y;
|
||||
}
|
||||
else
|
||||
{
|
||||
jumping.holdingJumpButton = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (grounded)
|
||||
{
|
||||
verticalVelocity = Vector3.zero;
|
||||
|
||||
if (!previouslyGrounded)
|
||||
{
|
||||
jumping.jumping = false;
|
||||
// Prefilled data is made to not hurt the player at any time when colliding with cyclops, but only to play the noise
|
||||
SendMessage(nameof(Player.OnLand), new MovementCollisionData
|
||||
{
|
||||
impactVelocity = Vector3.one,
|
||||
surfaceType = VFXSurfaceTypes.metal
|
||||
}, SendMessageOptions.DontRequireReceiver);
|
||||
}
|
||||
}
|
||||
// If player is no longer grounded after move
|
||||
else if (previouslyGrounded)
|
||||
{
|
||||
SendMessage("OnFall", SendMessageOptions.DontRequireReceiver);
|
||||
Pawn.Handle.transform.localPosition += step * Up;
|
||||
}
|
||||
|
||||
return cyclops.transform.rotation * latestVelocity;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates vertical velocity variation based on the grounded state.
|
||||
/// Code adapted from <see cref="GroundMotor.ApplyGravityAndJumping"/>.
|
||||
/// </summary>
|
||||
public Vector3 CalculateVerticalVelocity()
|
||||
{
|
||||
if (!jumpPressed)
|
||||
{
|
||||
jumping.holdingJumpButton = false;
|
||||
jumping.lastButtonDownTime = -100f;
|
||||
}
|
||||
if (jumpPressed && (jumping.lastButtonDownTime < 0f || flyCheatEnabled))
|
||||
{
|
||||
jumping.lastButtonDownTime = Time.time;
|
||||
}
|
||||
|
||||
Vector3 verticalMove = Vector3.zero;
|
||||
|
||||
if (!grounded)
|
||||
{
|
||||
verticalMove = -gravity * Up * DeltaTime;
|
||||
verticalMove.y = Mathf.Max(verticalMove.y, -movement.maxFallSpeed);
|
||||
}
|
||||
if (grounded || allowMidAirJumping || flyCheatEnabled)
|
||||
{
|
||||
if (Time.time - jumping.lastButtonDownTime < 0.2)
|
||||
{
|
||||
grounded = false;
|
||||
jumping.jumping = true;
|
||||
jumping.lastStartTime = Time.time;
|
||||
jumping.lastButtonDownTime = -100f;
|
||||
jumping.holdingJumpButton = true;
|
||||
Vector3 jumpDirection = Vector3.Slerp(Up, groundNormal, TooSteep() ? jumping.steepPerpAmount : jumping.perpAmount);
|
||||
verticalMove = jumpDirection * CalculateJumpVerticalSpeed(jumping.baseHeight);
|
||||
SendMessage("OnJump", SendMessageOptions.DontRequireReceiver);
|
||||
}
|
||||
else
|
||||
{
|
||||
jumping.holdingJumpButton = false;
|
||||
}
|
||||
}
|
||||
return verticalMove;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates instantaneous horizontal velocity from input for the <see cref="Pawn"/> object.
|
||||
/// Code adapted from <see cref="GroundMotor.ApplyInputVelocityChange"/>.
|
||||
/// </summary>
|
||||
public Vector3 CalculateInputVelocity()
|
||||
{
|
||||
// Project the movement input to the right rotation
|
||||
float moveMinMagnitude = Mathf.Min(1f, movementInputDirection.magnitude);
|
||||
|
||||
// We rotate the input in the right basis
|
||||
Vector3 input = movementInputDirection._X0Z();
|
||||
|
||||
Transform forwardRef = Pawn.Handle.transform;
|
||||
|
||||
Vector3 projectedForward = Vector3.ProjectOnPlane(forwardRef.forward, Up).normalized;
|
||||
Vector3 projectedRight = Vector3.ProjectOnPlane(forwardRef.right, Up).normalized;
|
||||
|
||||
Vector3 moveDirection = (projectedForward * input.z + projectedRight * input.x).normalized;
|
||||
|
||||
Vector3 velocity;
|
||||
// Manage sliding on slopes
|
||||
if (grounded && TooSteep())
|
||||
{
|
||||
velocity = GetSlidingDirection();
|
||||
Vector3 moveProjectedOnSlope = Vector3.Project(movementInputDirection, velocity);
|
||||
velocity += moveProjectedOnSlope * sliding.speedControl + (movementInputDirection - moveProjectedOnSlope) * sliding.sidewaysControl;
|
||||
velocity *= sliding.slidingSpeed;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Apply speed modifiers
|
||||
float modifier = 1f;
|
||||
if (sprintPressed && grounded)
|
||||
{
|
||||
float z = movementInputDirection.z;
|
||||
if (z > 0f)
|
||||
{
|
||||
modifier *= forwardSprintModifier;
|
||||
}
|
||||
else if (z == 0f)
|
||||
{
|
||||
modifier *= strafeSprintModifier;
|
||||
}
|
||||
sprinting = true;
|
||||
}
|
||||
velocity = moveDirection * forwardMaxSpeed * modifier * moveMinMagnitude;
|
||||
}
|
||||
if (XRSettings.enabled)
|
||||
{
|
||||
velocity *= VROptions.groundMoveScale;
|
||||
}
|
||||
|
||||
if (grounded)
|
||||
{
|
||||
velocity = AdjustGroundVelocityToNormal(velocity, groundNormal);
|
||||
}
|
||||
else
|
||||
{
|
||||
latestVelocity.y = 0f;
|
||||
}
|
||||
|
||||
float maxSpeed = GetMaxAcceleration(grounded) * DeltaTime;
|
||||
|
||||
Vector3 difference = velocity - latestVelocity;
|
||||
if (difference.sqrMagnitude > maxSpeed * maxSpeed)
|
||||
{
|
||||
difference = difference.normalized * maxSpeed;
|
||||
}
|
||||
latestVelocity += difference;
|
||||
|
||||
if (grounded)
|
||||
{
|
||||
latestVelocity.y = Mathf.Min(latestVelocity.y, 0f);
|
||||
}
|
||||
|
||||
return latestVelocity;
|
||||
}
|
||||
|
||||
private new Vector3 GetSlidingDirection()
|
||||
{
|
||||
return Vector3.ProjectOnPlane(groundNormal, Up).normalized;
|
||||
}
|
||||
|
||||
private new bool TooSteep()
|
||||
{
|
||||
float dotUp = Vector3.Dot(groundNormal, Up);
|
||||
return dotUp <= Mathf.Cos(controller.slopeLimit * Mathf.Deg2Rad);
|
||||
}
|
||||
|
||||
public void ToggleCyclopsMotor(bool toggled)
|
||||
{
|
||||
GroundMotor groundMotor = toggled ? this : ActualMotor;
|
||||
Player.main.playerController.SetEnabled(false);
|
||||
Player.main.groundMotor = groundMotor;
|
||||
|
||||
Player.main.footStepSounds.groundMoveable = groundMotor;
|
||||
Player.main.groundMotor = groundMotor;
|
||||
Player.main.playerController.groundController = groundMotor;
|
||||
if (Player.main.playerController.activeController is GroundMotor)
|
||||
{
|
||||
Player.main.playerController.activeController = groundMotor;
|
||||
}
|
||||
// SetMotorMode sets some important variables in the motor abstract class PlayerMotor
|
||||
Player.main.playerController.SetMotorMode(Player.MotorMode.Walk);
|
||||
|
||||
Player.main.playerController.SetEnabled(true);
|
||||
}
|
||||
}
|
116
NitroxClient/MonoBehaviours/Cyclops/CyclopsMotorGroundChecker.cs
Normal file
116
NitroxClient/MonoBehaviours/Cyclops/CyclopsMotorGroundChecker.cs
Normal file
@@ -0,0 +1,116 @@
|
||||
using NitroxClient.GameLogic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace NitroxClient.MonoBehaviours;
|
||||
|
||||
/// <remarks>
|
||||
/// Ground detection adapted from <see href="https://github.com/Unity-Technologies/Standard-Assets-Characters/blob/master/Assets/_Standard%20Assets/Characters/Scripts/Physics/OpenCharacterController.cs"/>
|
||||
/// </remarks>
|
||||
public partial class CyclopsMotor
|
||||
{
|
||||
private const float CAST_DISTANCE = 0.001f;
|
||||
private const float CAST_EXTRA_DISTANCE = 0.001f;
|
||||
|
||||
public const QueryTriggerInteraction QuerySetting = QueryTriggerInteraction.Ignore;
|
||||
public static readonly int LayerMaskExceptPlayer = ~CyclopsPawn.PLAYER_LAYER;
|
||||
|
||||
/// <summary>
|
||||
/// Latest snapshot of the Pawn's global position. It is updated every frame before being used.
|
||||
/// </summary>
|
||||
public Vector3 Position;
|
||||
/// <summary>
|
||||
/// Latest snapshot of the globally transformed center offset. It is updated every frame before being used.
|
||||
/// </summary>
|
||||
public Vector3 Center;
|
||||
/// <summary>
|
||||
/// <see cref="CharacterController.height"/> scaled by the transform's y global scale
|
||||
/// </summary>
|
||||
public float Height;
|
||||
/// <summary>
|
||||
/// <see cref="CharacterController.radius"/> scaled by the transform's maximum global scale parameter
|
||||
/// </summary>
|
||||
public float Radius;
|
||||
/// <summary>
|
||||
/// Unscaled <see cref="CharacterController.skinWidth"/>
|
||||
/// </summary>
|
||||
public float SkinWidth;
|
||||
/// <summary>
|
||||
/// Snapshot of the latest <see cref="CollisionFlags"/> obtained when simulating movement on the pawn.
|
||||
/// </summary>
|
||||
private CollisionFlags Collision { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Checks if Pawn is grounded by up to 2 sphere casts. Updates the registered ground normal accordingly.
|
||||
/// </summary>
|
||||
public void CheckGrounded(CollisionFlags flags, bool cast)
|
||||
{
|
||||
if (cast)
|
||||
{
|
||||
Vector3 lowerPoint = GetLowerPoint();
|
||||
|
||||
grounded = false;
|
||||
if (SphereCast(-Up, SkinWidth + CAST_DISTANCE, out RaycastHit hitInfo, lowerPoint, false))
|
||||
{
|
||||
grounded = true;
|
||||
hitInfo.distance = Mathf.Max(0f, hitInfo.distance - SkinWidth);
|
||||
}
|
||||
|
||||
if (!grounded && SphereCast(-Up, CAST_DISTANCE + CAST_EXTRA_DISTANCE, out hitInfo, lowerPoint + Up * CAST_EXTRA_DISTANCE, true))
|
||||
{
|
||||
grounded = true;
|
||||
hitInfo.distance = Mathf.Max(0f, hitInfo.distance - SkinWidth);
|
||||
}
|
||||
|
||||
groundNormal = hitInfo.normal;
|
||||
return;
|
||||
}
|
||||
|
||||
// Exceptional case in which movement was made on the ground but the casts failed
|
||||
if (flags == CollisionFlags.Below)
|
||||
{
|
||||
grounded = true;
|
||||
groundNormal = Up;
|
||||
return;
|
||||
}
|
||||
|
||||
grounded = false;
|
||||
groundNormal = Vector3.zero;
|
||||
}
|
||||
|
||||
public bool SphereCast(Vector3 direction, float distance, out RaycastHit hitInfo, Vector3 spherePosition, bool big)
|
||||
{
|
||||
float radius = big ? Radius + SkinWidth : Radius;
|
||||
|
||||
if (Physics.SphereCast(spherePosition, radius, direction, out hitInfo, distance + radius, LayerMaskExceptPlayer, QuerySetting))
|
||||
{
|
||||
return hitInfo.distance <= distance;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public Vector3 GetLowerPoint()
|
||||
{
|
||||
return Position + Center - Up * (Height * 0.5f - Radius);
|
||||
}
|
||||
|
||||
public override void SetControllerHeight(float height, float cameraOffset)
|
||||
{
|
||||
base.SetControllerHeight(height, cameraOffset);
|
||||
RecalculateConstants();
|
||||
}
|
||||
|
||||
public override void SetControllerRadius(float radius)
|
||||
{
|
||||
base.SetControllerRadius(radius);
|
||||
RecalculateConstants();
|
||||
}
|
||||
|
||||
private void RecalculateConstants()
|
||||
{
|
||||
Vector3 scale = transform.lossyScale;
|
||||
Height = controller.height * scale.y;
|
||||
Radius = controller.radius * Mathf.Max(Mathf.Max(scale.x, scale.y), scale.z);
|
||||
SkinWidth = controller.skinWidth;
|
||||
}
|
||||
}
|
177
NitroxClient/MonoBehaviours/Cyclops/NitroxCyclops.cs
Normal file
177
NitroxClient/MonoBehaviours/Cyclops/NitroxCyclops.cs
Normal file
@@ -0,0 +1,177 @@
|
||||
using System.Collections.Generic;
|
||||
using NitroxClient.GameLogic;
|
||||
using NitroxClient.GameLogic.PlayerLogic;
|
||||
using NitroxClient.GameLogic.PlayerLogic.PlayerModel.Abstract;
|
||||
using UnityEngine;
|
||||
|
||||
namespace NitroxClient.MonoBehaviours.Cyclops;
|
||||
|
||||
/// <summary>
|
||||
/// Script responsible for managing all player movement-related interactions.
|
||||
/// </summary>
|
||||
public class NitroxCyclops : MonoBehaviour
|
||||
{
|
||||
public VirtualCyclops Virtual;
|
||||
private CyclopsMotor cyclopsMotor;
|
||||
private SubRoot subRoot;
|
||||
private SubControl subControl;
|
||||
private Rigidbody rigidbody;
|
||||
private WorldForces worldForces;
|
||||
private Stabilizer stabilizer;
|
||||
private CharacterController controller;
|
||||
private CyclopsNoiseManager cyclopsNoiseManager;
|
||||
|
||||
public readonly Dictionary<INitroxPlayer, CyclopsPawn> Pawns = [];
|
||||
|
||||
public static readonly Dictionary<NitroxCyclops, float> ScaledNoiseByCyclops = [];
|
||||
|
||||
public void Start()
|
||||
{
|
||||
cyclopsMotor = Player.mainObject.GetComponent<CyclopsMotor>();
|
||||
subRoot = GetComponent<SubRoot>();
|
||||
subControl = GetComponent<SubControl>();
|
||||
rigidbody = GetComponent<Rigidbody>();
|
||||
worldForces = GetComponent<WorldForces>();
|
||||
stabilizer = GetComponent<Stabilizer>();
|
||||
controller = cyclopsMotor.controller;
|
||||
cyclopsNoiseManager = GetComponent<CyclopsNoiseManager>();
|
||||
|
||||
UWE.Utils.SetIsKinematicAndUpdateInterpolation(rigidbody, false, true);
|
||||
|
||||
WorkaroundColliders();
|
||||
|
||||
ScaledNoiseByCyclops.Add(this, 0f);
|
||||
}
|
||||
|
||||
public void Update()
|
||||
{
|
||||
MaintainPawns();
|
||||
|
||||
// Calculation from AttackCyclops.UpdateAggression
|
||||
ScaledNoiseByCyclops[this] = Mathf.Lerp(0f, 150f, cyclopsNoiseManager.GetNoisePercent());
|
||||
}
|
||||
|
||||
public void OnDestroy()
|
||||
{
|
||||
ScaledNoiseByCyclops.Remove(this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Triggers required on-remove callbacks on children player objects, including the local player.
|
||||
/// </summary>
|
||||
public void RemoveAllPlayers()
|
||||
{
|
||||
// This will call OnLocalPlayerExit
|
||||
if (Player.main.currentSub == subRoot)
|
||||
{
|
||||
Player.main.SetCurrentSub(null);
|
||||
}
|
||||
|
||||
foreach (RemotePlayerIdentifier remotePlayerIdentifier in GetComponentsInChildren<RemotePlayerIdentifier>(true))
|
||||
{
|
||||
remotePlayerIdentifier.RemotePlayer.ResetStates();
|
||||
OnPlayerExit(remotePlayerIdentifier.RemotePlayer);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parents local player to the cyclops and registers it in the current cyclops.
|
||||
/// </summary>
|
||||
public void OnLocalPlayerEnter()
|
||||
{
|
||||
Virtual = VirtualCyclops.Instance;
|
||||
Virtual.SetCurrentCyclops(this);
|
||||
|
||||
Player.mainObject.transform.parent = subRoot.transform;
|
||||
CyclopsPawn pawn = AddPawnForPlayer(this.Resolve<ILocalNitroxPlayer>());
|
||||
cyclopsMotor.SetCyclops(this, subRoot, pawn);
|
||||
cyclopsMotor.ToggleCyclopsMotor(true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unregisters the local player from the current cyclops. Ensures the player is not weirdly rotated when it leaves the cyclops.
|
||||
/// </summary>
|
||||
public void OnLocalPlayerExit()
|
||||
{
|
||||
RemovePawnForPlayer(this.Resolve<ILocalNitroxPlayer>());
|
||||
Player.main.transform.parent = null;
|
||||
Player.main.transform.rotation = Quaternion.identity;
|
||||
cyclopsMotor.ToggleCyclopsMotor(false);
|
||||
cyclopsMotor.Pawn = null;
|
||||
|
||||
if (Virtual)
|
||||
{
|
||||
Virtual.SetCurrentCyclops(null);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers a remote player for it to get a pawn in the current cyclops.
|
||||
/// </summary>
|
||||
public void OnPlayerEnter(RemotePlayer remotePlayer)
|
||||
{
|
||||
remotePlayer.Pawn = AddPawnForPlayer(remotePlayer);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unregisters a remote player from the current cyclops.
|
||||
/// </summary>
|
||||
public void OnPlayerExit(RemotePlayer remotePlayer)
|
||||
{
|
||||
RemovePawnForPlayer(remotePlayer);
|
||||
remotePlayer.Pawn = null;
|
||||
}
|
||||
|
||||
public void MaintainPawns()
|
||||
{
|
||||
foreach (CyclopsPawn pawn in Pawns.Values)
|
||||
{
|
||||
if (pawn.MaintainPredicate())
|
||||
{
|
||||
pawn.MaintainPosition();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public CyclopsPawn AddPawnForPlayer(INitroxPlayer player)
|
||||
{
|
||||
if (!Pawns.TryGetValue(player, out CyclopsPawn pawn))
|
||||
{
|
||||
pawn = new(player, this);
|
||||
Pawns.Add(player, pawn);
|
||||
}
|
||||
return pawn;
|
||||
}
|
||||
|
||||
public void RemovePawnForPlayer(INitroxPlayer player)
|
||||
{
|
||||
if (Pawns.TryGetValue(player, out CyclopsPawn pawn))
|
||||
{
|
||||
pawn.Terminate();
|
||||
}
|
||||
Pawns.Remove(player);
|
||||
}
|
||||
|
||||
public void SetBroadcasting()
|
||||
{
|
||||
worldForces.enabled = true;
|
||||
stabilizer.stabilizerEnabled = true;
|
||||
}
|
||||
|
||||
public void SetReceiving()
|
||||
{
|
||||
worldForces.enabled = false;
|
||||
stabilizer.stabilizerEnabled = false;
|
||||
}
|
||||
|
||||
private void WorkaroundColliders()
|
||||
{
|
||||
CyclopsSubNameScreen cyclopsSubNameScreen = transform.GetComponentInChildren<CyclopsSubNameScreen>(true);
|
||||
TriggerWorkaround subNameTriggerWorkaround = cyclopsSubNameScreen.gameObject.AddComponent<TriggerWorkaround>();
|
||||
subNameTriggerWorkaround.Initialize(this,cyclopsSubNameScreen.animator, cyclopsSubNameScreen.ContentOn, nameof(CyclopsSubNameScreen.ContentOff), cyclopsSubNameScreen);
|
||||
|
||||
CyclopsLightingPanel cyclopsLightingPanel = transform.GetComponentInChildren<CyclopsLightingPanel>(true);
|
||||
TriggerWorkaround lightingTriggerWorkaround = cyclopsLightingPanel.gameObject.AddComponent<TriggerWorkaround>();
|
||||
lightingTriggerWorkaround.Initialize(this, cyclopsLightingPanel.uiPanel, cyclopsLightingPanel.ButtonsOn, nameof(CyclopsLightingPanel.ButtonsOff), cyclopsLightingPanel);
|
||||
}
|
||||
}
|
59
NitroxClient/MonoBehaviours/Cyclops/TriggerWorkaround.cs
Normal file
59
NitroxClient/MonoBehaviours/Cyclops/TriggerWorkaround.cs
Normal file
@@ -0,0 +1,59 @@
|
||||
using System;
|
||||
using UnityEngine;
|
||||
|
||||
namespace NitroxClient.MonoBehaviours.Cyclops;
|
||||
|
||||
/// <summary>
|
||||
/// With the changes to the Player's colliders, the cyclops doesn't detect the player entering or leaving to triggers
|
||||
/// The easiest workaround is to replace proximity detection by distance checks.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Works for <see cref="CyclopsLightingPanel"/> and <see cref="CyclopsSubNameScreen"/>.
|
||||
/// </remarks>
|
||||
public class TriggerWorkaround : MonoBehaviour
|
||||
{
|
||||
private const float DETECTION_RANGE = 5f;
|
||||
private const string ANIMATOR_PARAM = "PanelActive";
|
||||
private bool playerIn;
|
||||
|
||||
private NitroxCyclops cyclops;
|
||||
private Animator animator;
|
||||
private Action onEnterCallback;
|
||||
private string onExitInvokeCallback;
|
||||
private MonoBehaviour targetBehaviour;
|
||||
|
||||
public void Initialize(NitroxCyclops cyclops, Animator animator, Action onEnterCallback, string onExitInvokeCallback, MonoBehaviour targetBehaviour)
|
||||
{
|
||||
this.cyclops = cyclops;
|
||||
this.animator = animator;
|
||||
this.onEnterCallback = onEnterCallback;
|
||||
this.onExitInvokeCallback = onExitInvokeCallback;
|
||||
this.targetBehaviour = targetBehaviour;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Code adapted from <see cref="CyclopsSubNameScreen.OnTriggerEnter"/> and <see cref="CyclopsSubNameScreen.OnTriggerExit"/>
|
||||
/// </summary>
|
||||
public void Update()
|
||||
{
|
||||
// Virtual is not null only when the local player is aboard
|
||||
if (cyclops.Virtual && Vector3.Distance(Player.main.transform.position, transform.position) < DETECTION_RANGE)
|
||||
{
|
||||
if (!playerIn)
|
||||
{
|
||||
playerIn = true;
|
||||
animator.SetBool(ANIMATOR_PARAM, true);
|
||||
onEnterCallback();
|
||||
targetBehaviour.CancelInvoke(onExitInvokeCallback);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (playerIn)
|
||||
{
|
||||
playerIn = false;
|
||||
animator.SetBool(ANIMATOR_PARAM, false);
|
||||
targetBehaviour.Invoke(onExitInvokeCallback, 0.5f);
|
||||
}
|
||||
}
|
||||
}
|
290
NitroxClient/MonoBehaviours/Cyclops/VirtualCyclops.cs
Normal file
290
NitroxClient/MonoBehaviours/Cyclops/VirtualCyclops.cs
Normal file
@@ -0,0 +1,290 @@
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NitroxClient.Communication;
|
||||
using NitroxClient.GameLogic.Spawning.WorldEntities;
|
||||
using NitroxModel.Packets;
|
||||
using UnityEngine;
|
||||
|
||||
namespace NitroxClient.MonoBehaviours.Cyclops;
|
||||
|
||||
/// <summary>
|
||||
/// Script responsible for creating a virtual counterpart of every cyclops, which will always be horizontal and motionless so that simulated movement is always clear.
|
||||
/// Contains a pawn for each player entering the regular cyclops.
|
||||
/// </summary>
|
||||
public class VirtualCyclops : MonoBehaviour
|
||||
{
|
||||
public static VirtualCyclops Instance;
|
||||
public const string NAME = "VirtualCyclops";
|
||||
|
||||
private static readonly Dictionary<TechType, GameObject> cacheColliderCopy = [];
|
||||
private readonly Dictionary<string, Openable> virtualOpenableByName = [];
|
||||
private readonly Dictionary<string, Openable> realOpenableByName = [];
|
||||
private readonly Dictionary<GameObject, GameObject> virtualConstructableByRealGameObject = [];
|
||||
public NitroxCyclops Cyclops;
|
||||
public Transform axis;
|
||||
|
||||
private Rigidbody rigidbody;
|
||||
private Vector3 InitialPosition;
|
||||
private Quaternion InitialRotation;
|
||||
|
||||
public static void Initialize()
|
||||
{
|
||||
CreateVirtualCyclops();
|
||||
Multiplayer.OnAfterMultiplayerEnd += Dispose;
|
||||
}
|
||||
|
||||
public static void Dispose()
|
||||
{
|
||||
Destroy(Instance.gameObject);
|
||||
Instance = null;
|
||||
Multiplayer.OnAfterMultiplayerEnd -= Dispose;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes the <see cref="Prefab"/> object with reduced utility to ensure the virtual cyclops won't be eating too much performance.
|
||||
/// </summary>
|
||||
public static void CreateVirtualCyclops()
|
||||
{
|
||||
if (Instance)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
LightmappedPrefabs.main.RequestScenePrefab("cyclops", (cyclopsPrefab) =>
|
||||
{
|
||||
SubConsoleCommand.main.OnSubPrefabLoaded(cyclopsPrefab);
|
||||
GameObject model = SubConsoleCommand.main.GetLastCreatedSub();
|
||||
model.name = NAME;
|
||||
Vector3 position = Vector3.up * 500;
|
||||
Quaternion rotation = Quaternion.identity;
|
||||
model.transform.position = position;
|
||||
model.transform.rotation = rotation;
|
||||
|
||||
Instance = model.AddComponent<VirtualCyclops>();
|
||||
|
||||
Instance.axis = model.GetComponent<SubRoot>().subAxis;
|
||||
|
||||
GameObject.Destroy(model.GetComponent<EcoTarget>());
|
||||
GameObject.Destroy(model.GetComponent<PingInstance>());
|
||||
GameObject.Destroy(model.GetComponent<CyclopsDestructionEvent>());
|
||||
GameObject.Destroy(model.GetComponent<VFXConstructing>());
|
||||
|
||||
Instance.InitialPosition = position;
|
||||
Instance.InitialRotation = rotation;
|
||||
Instance.rigidbody = Instance.GetComponent<Rigidbody>();
|
||||
Instance.rigidbody.constraints = RigidbodyConstraints.FreezeAll;
|
||||
|
||||
model.GetComponent<WorldForces>().enabled = false;
|
||||
model.GetComponent<WorldForces>().lockInterpolation = false;
|
||||
model.GetComponent<Stabilizer>().stabilizerEnabled = false;
|
||||
model.GetComponent<Rigidbody>().isKinematic = true;
|
||||
model.GetComponent<LiveMixin>().invincible = true;
|
||||
|
||||
Instance.RegisterVirtualOpenables();
|
||||
Instance.ToggleRenderers(false);
|
||||
Instance.DisableBadComponents();
|
||||
|
||||
model.SetActive(true);
|
||||
});
|
||||
}
|
||||
|
||||
public static IEnumerator InitializeConstructablesCache()
|
||||
{
|
||||
List<TechType> constructableTechTypes = [];
|
||||
CraftData.GetBuilderGroupTech(TechGroup.InteriorModules, constructableTechTypes, true);
|
||||
CraftData.GetBuilderGroupTech(TechGroup.Miscellaneous, constructableTechTypes, true);
|
||||
|
||||
TaskResult<GameObject> result = new();
|
||||
foreach (TechType techType in constructableTechTypes)
|
||||
{
|
||||
yield return DefaultWorldEntitySpawner.RequestPrefab(techType, result);
|
||||
if (result.value && result.value.GetComponent<Constructable>())
|
||||
{
|
||||
// We immediately destroy the copy because we only want to cache it for now
|
||||
Destroy(CreateColliderCopy(result.value, techType));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Populate()
|
||||
{
|
||||
foreach (Constructable constructable in Cyclops.GetComponentsInChildren<Constructable>(true))
|
||||
{
|
||||
ReplicateConstructable(constructable);
|
||||
}
|
||||
|
||||
foreach (Openable openable in Cyclops.GetComponentsInChildren<Openable>(true))
|
||||
{
|
||||
openable.blocked = false;
|
||||
ReplicateOpening(openable, openable.isOpen);
|
||||
realOpenableByName.Add(openable.name, openable);
|
||||
}
|
||||
}
|
||||
|
||||
public void Depopulate()
|
||||
{
|
||||
foreach (GameObject virtualObject in virtualConstructableByRealGameObject.Values)
|
||||
{
|
||||
Destroy(virtualObject);
|
||||
}
|
||||
virtualConstructableByRealGameObject.Clear();
|
||||
|
||||
foreach (Openable openable in realOpenableByName.Values)
|
||||
{
|
||||
openable.blocked = false;
|
||||
}
|
||||
realOpenableByName.Clear();
|
||||
}
|
||||
|
||||
public void SetCurrentCyclops(NitroxCyclops nitroxCyclops)
|
||||
{
|
||||
if (Cyclops)
|
||||
{
|
||||
Cyclops.Virtual = null;
|
||||
Depopulate();
|
||||
Cyclops = null;
|
||||
}
|
||||
|
||||
Cyclops = nitroxCyclops;
|
||||
if (Cyclops)
|
||||
{
|
||||
Populate();
|
||||
}
|
||||
}
|
||||
|
||||
public void Update()
|
||||
{
|
||||
transform.position = InitialPosition;
|
||||
transform.rotation = InitialRotation;
|
||||
}
|
||||
|
||||
public void ToggleRenderers(bool toggled)
|
||||
{
|
||||
foreach (Renderer renderer in transform.GetComponentsInChildren<Renderer>(true))
|
||||
{
|
||||
renderer.enabled = toggled;
|
||||
}
|
||||
}
|
||||
|
||||
private void RegisterVirtualOpenables()
|
||||
{
|
||||
foreach (Openable openable in transform.GetComponentsInChildren<Openable>(true))
|
||||
{
|
||||
virtualOpenableByName.Add(openable.name, openable);
|
||||
}
|
||||
}
|
||||
|
||||
private void DisableBadComponents()
|
||||
{
|
||||
CyclopsLightingPanel cyclopsLightingPanel = GetComponentInChildren<CyclopsLightingPanel>(true);
|
||||
cyclopsLightingPanel.floodlightsOn = false;
|
||||
cyclopsLightingPanel.lightingOn = false;
|
||||
cyclopsLightingPanel.SetExternalLighting(false);
|
||||
cyclopsLightingPanel.cyclopsRoot.ForceLightingState(false);
|
||||
cyclopsLightingPanel.enabled = false;
|
||||
Destroy(cyclopsLightingPanel);
|
||||
|
||||
// Disable a source of useless logs
|
||||
foreach (FMOD_CustomEmitter customEmitter in GetComponentsInChildren<FMOD_CustomEmitter>(true))
|
||||
{
|
||||
customEmitter.enabled = false;
|
||||
}
|
||||
foreach (PlayerCinematicController cinematicController in GetComponentsInChildren<PlayerCinematicController>(true))
|
||||
{
|
||||
cinematicController.enabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
public void ReplicateOpening(Openable openable, bool openState)
|
||||
{
|
||||
if (virtualOpenableByName.TryGetValue(openable.name, out Openable virtualOpenable))
|
||||
{
|
||||
using (PacketSuppressor<OpenableStateChanged>.Suppress())
|
||||
{
|
||||
virtualOpenable.PlayOpenAnimation(openState, virtualOpenable.animTime);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void ReplicateBlock(Openable openable, bool blockState)
|
||||
{
|
||||
if (realOpenableByName.TryGetValue(openable.name, out Openable realOpenable))
|
||||
{
|
||||
realOpenable.blocked = blockState;
|
||||
}
|
||||
}
|
||||
|
||||
public void ReplicateConstructable(Constructable constructable)
|
||||
{
|
||||
if (virtualConstructableByRealGameObject.ContainsKey(constructable.gameObject))
|
||||
{
|
||||
return;
|
||||
}
|
||||
GameObject colliderCopy = CreateColliderCopy(constructable.gameObject, constructable.techType);
|
||||
colliderCopy.transform.parent = transform;
|
||||
colliderCopy.transform.CopyLocals(constructable.transform);
|
||||
|
||||
virtualConstructableByRealGameObject.Add(constructable.gameObject, colliderCopy);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates an empty shell simulating the presence of modules by copying its children containing a collider.
|
||||
/// </summary>
|
||||
public static GameObject CreateColliderCopy(GameObject realObject, TechType techType)
|
||||
{
|
||||
if (cacheColliderCopy.TryGetValue(techType, out GameObject colliderCopy))
|
||||
{
|
||||
return GameObject.Instantiate(colliderCopy);
|
||||
}
|
||||
colliderCopy = new GameObject($"{realObject.name}-collidercopy");
|
||||
// This will act as a prefab but will stay in the material world so we put it out of hands in the meantime
|
||||
colliderCopy.transform.position = Vector3.up * 1000 + Vector3.right * 10 * cacheColliderCopy.Count;
|
||||
|
||||
Transform transform = realObject.transform;
|
||||
|
||||
Dictionary<Transform, Transform> copiedTransformByRealTransform = [];
|
||||
copiedTransformByRealTransform[transform] = colliderCopy.transform;
|
||||
|
||||
IEnumerable<GameObject> uniqueColliderObjects = realObject.GetComponentsInChildren<Collider>(true).Select(c => c.gameObject).Distinct();
|
||||
foreach (GameObject colliderObject in uniqueColliderObjects)
|
||||
{
|
||||
GameObject copiedColliderObject = new(colliderObject.name);
|
||||
copiedColliderObject.transform.CopyLocals(colliderObject.transform);
|
||||
foreach (Collider collider in colliderObject.GetComponents<Collider>())
|
||||
{
|
||||
collider.CopyComponent(copiedColliderObject);
|
||||
}
|
||||
|
||||
// "child" is always a copied transform looking for its copied parent
|
||||
Transform child = copiedColliderObject.transform;
|
||||
// "parent" is always the real parent of the real transform corresponding to "child"
|
||||
Transform parent = colliderObject.transform.parent;
|
||||
|
||||
while (!copiedTransformByRealTransform.ContainsKey(parent))
|
||||
{
|
||||
Transform copiedParent = copiedTransformByRealTransform[parent] = Instantiate(parent);
|
||||
|
||||
child.SetParent(copiedParent, false);
|
||||
|
||||
child = copiedParent;
|
||||
parent = parent.parent;
|
||||
}
|
||||
|
||||
// At the top of the tree we can simply stick the latest child to the collider
|
||||
child.SetParent(colliderCopy.transform, false);
|
||||
}
|
||||
|
||||
cacheColliderCopy.Add(techType, colliderCopy);
|
||||
return GameObject.Instantiate(colliderCopy);
|
||||
}
|
||||
|
||||
public void UnregisterConstructable(GameObject realObject)
|
||||
{
|
||||
if (virtualConstructableByRealGameObject.TryGetValue(realObject, out GameObject virtualConstructable))
|
||||
{
|
||||
Destroy(virtualConstructable);
|
||||
virtualConstructableByRealGameObject.Remove(realObject);
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user