250 lines
8.4 KiB
C#
250 lines
8.4 KiB
C#
using UnityEngine;
|
|
using Mirror;
|
|
using UnityEngine.InputSystem;
|
|
|
|
public class CharacterMovement : NetworkBehaviour
|
|
{
|
|
Animator animator;
|
|
|
|
int isWalkingHash;
|
|
int isRunningHash;
|
|
|
|
PlayerInput input;
|
|
|
|
Vector2 currentMovement;
|
|
Vector2 combinedMovement;
|
|
bool movementPressed;
|
|
bool runPressed;
|
|
|
|
// track last sent state to avoid network spam
|
|
bool lastSentWalking;
|
|
bool lastSentRunning;
|
|
|
|
// assign this in the player prefab to an empty child transform positioned where the camera should sit (e.g. head)
|
|
public Transform cameraMount;
|
|
|
|
// camera follow settings
|
|
Camera mainCamera;
|
|
Vector3 cameraLocalOffsetStable = new Vector3(0f, 2f, -4f); // fallback offset (local)
|
|
public float followSpeed = 5f;
|
|
public float idleFollowSpeed = 0.5f; // slower smoothing while idle (keeps camera stable)
|
|
public Vector3 cameraLookOffset = new Vector3(0f, 1.5f, 0f); // where camera looks relative to player root
|
|
|
|
void Awake()
|
|
{
|
|
input = new PlayerInput();
|
|
|
|
input.CharacterControls.Movement.performed += ctx =>
|
|
{
|
|
currentMovement = ctx.ReadValue<Vector2>();
|
|
movementPressed = currentMovement.x != 0 || currentMovement.y != 0;
|
|
};
|
|
input.CharacterControls.Movement.canceled += ctx =>
|
|
{
|
|
currentMovement = Vector2.zero;
|
|
movementPressed = false;
|
|
};
|
|
|
|
input.CharacterControls.Run.performed += ctx => runPressed = ctx.ReadValueAsButton();
|
|
input.CharacterControls.Run.canceled += ctx => runPressed = false;
|
|
}
|
|
|
|
void Start()
|
|
{
|
|
animator = GetComponent<Animator>();
|
|
|
|
isWalkingHash = Animator.StringToHash("isWalking");
|
|
isRunningHash = Animator.StringToHash("isRunning");
|
|
}
|
|
|
|
// Only the local player should drive input/movement
|
|
void Update()
|
|
{
|
|
if (!isLocalPlayer) return;
|
|
|
|
HandleMovement();
|
|
HandleRotation();
|
|
}
|
|
|
|
// Camera follow should be in LateUpdate so it follows the final character pose for the frame
|
|
void LateUpdate()
|
|
{
|
|
if (!isLocalPlayer) return;
|
|
if (mainCamera == null) return;
|
|
|
|
// Desired world position based on the stable local offset (relative to player root)
|
|
Vector3 desiredWorld = transform.TransformPoint(cameraLocalOffsetStable);
|
|
|
|
if (movementPressed)
|
|
{
|
|
// while moving: follow the player (smooth)
|
|
mainCamera.transform.position = Vector3.Lerp(mainCamera.transform.position, desiredWorld, Time.deltaTime * followSpeed);
|
|
}
|
|
else
|
|
{
|
|
// while idle: keep camera stable behind the player (don't follow micro animation bobbing)
|
|
// Update only XZ to remain behind, preserve camera's current Y (height) to avoid vertical bob
|
|
Vector3 idleTarget = new Vector3(desiredWorld.x, mainCamera.transform.position.y, desiredWorld.z);
|
|
mainCamera.transform.position = Vector3.Lerp(mainCamera.transform.position, idleTarget, Time.deltaTime * idleFollowSpeed);
|
|
}
|
|
|
|
// Always look at the player (with an offset so we look near the torso/head)
|
|
mainCamera.transform.LookAt(transform.position + cameraLookOffset);
|
|
}
|
|
|
|
void HandleRotation()
|
|
{
|
|
Vector3 currentPosition = transform.position;
|
|
Vector3 newPosition = new Vector3(combinedMovement.x, 0, combinedMovement.y);
|
|
// only rotate when there is movement to look at
|
|
if (newPosition.sqrMagnitude > 0f)
|
|
{
|
|
Vector3 positionToLookAt = currentPosition + newPosition;
|
|
transform.LookAt(positionToLookAt);
|
|
}
|
|
}
|
|
|
|
void HandleMovement()
|
|
{
|
|
// read keyboard WASD (supplements / works alongside Input System action bindings)
|
|
Vector2 keyboardMovement = Vector2.zero;
|
|
var kb = Keyboard.current;
|
|
if (kb != null)
|
|
{
|
|
if (kb.wKey.isPressed) keyboardMovement.y += 1f;
|
|
if (kb.sKey.isPressed) keyboardMovement.y -= 1f;
|
|
if (kb.aKey.isPressed) keyboardMovement.x -= 1f;
|
|
if (kb.dKey.isPressed) keyboardMovement.x += 1f;
|
|
}
|
|
|
|
// Disable keyboard WASD while typing in chat input
|
|
if (ChatUI.IsInputFieldFocused)
|
|
{
|
|
keyboardMovement = Vector2.zero;
|
|
}
|
|
|
|
// combine input action movement and keyboard movement
|
|
combinedMovement = currentMovement + keyboardMovement;
|
|
// prevent faster diagonal movement
|
|
combinedMovement = Vector2.ClampMagnitude(combinedMovement, 1f);
|
|
movementPressed = combinedMovement.x != 0 || combinedMovement.y != 0;
|
|
|
|
// detect Shift keys as an additional run input
|
|
bool shiftPressed = kb != null && (kb.leftShiftKey.isPressed || kb.rightShiftKey.isPressed);
|
|
bool runActive = runPressed || shiftPressed;
|
|
|
|
bool isRunning = animator.GetBool(isRunningHash);
|
|
bool isWalking = animator.GetBool(isWalkingHash);
|
|
|
|
if (movementPressed && !isWalking)
|
|
{
|
|
animator.SetBool(isWalkingHash, true);
|
|
isWalking = true;
|
|
}
|
|
|
|
if (!movementPressed && isWalking)
|
|
{
|
|
animator.SetBool(isWalkingHash, false);
|
|
isWalking = false;
|
|
}
|
|
|
|
if ((movementPressed && runActive) && !isRunning)
|
|
{
|
|
animator.SetBool(isRunningHash, true);
|
|
isRunning = true;
|
|
}
|
|
|
|
if ((!movementPressed || !runActive) && isRunning)
|
|
{
|
|
animator.SetBool(isRunningHash, false);
|
|
isRunning = false;
|
|
}
|
|
|
|
// send animator state to server only when changed
|
|
if (isWalking != lastSentWalking || isRunning != lastSentRunning)
|
|
{
|
|
lastSentWalking = isWalking;
|
|
lastSentRunning = isRunning;
|
|
CmdSendAnimationState(isWalking, isRunning);
|
|
}
|
|
|
|
// movement translation (local)
|
|
float speed = isRunning ? 6f : 3f;
|
|
Vector3 move = new Vector3(combinedMovement.x, 0, combinedMovement.y) * speed * Time.deltaTime;
|
|
transform.position += move;
|
|
}
|
|
|
|
public override void OnStartLocalPlayer()
|
|
{
|
|
Debug.Log("Local player started: " + netId);
|
|
input.CharacterControls.Enable();
|
|
|
|
// Setup camera for local player (do not parent to animated mount to avoid animation bobbing)
|
|
mainCamera = Camera.main;
|
|
if (mainCamera == null)
|
|
{
|
|
Debug.LogWarning("No Camera.main found in scene to attach to player.");
|
|
return;
|
|
}
|
|
|
|
// compute a stable local offset based on cameraMount, but use player root as the reference so animated child motion is ignored
|
|
if (cameraMount != null)
|
|
{
|
|
cameraLocalOffsetStable = transform.InverseTransformPoint(cameraMount.position);
|
|
}
|
|
else
|
|
{
|
|
// fallback to a sensible offset if no mount assigned
|
|
cameraLocalOffsetStable = new Vector3(0f, 2f, -4f);
|
|
Debug.LogWarning("cameraMount not assigned on player prefab. Using fallback offset.");
|
|
}
|
|
|
|
// unparent camera so we control world position directly
|
|
mainCamera.transform.SetParent(null, true);
|
|
|
|
// ensure only the local player's camera has an active AudioListener
|
|
var audio = mainCamera.GetComponent<AudioListener>();
|
|
if (audio != null) audio.enabled = true;
|
|
}
|
|
|
|
public override void OnStopLocalPlayer()
|
|
{
|
|
input.CharacterControls.Disable();
|
|
|
|
// optionally disable audio listener to avoid duplicates
|
|
if (mainCamera != null)
|
|
{
|
|
var audio = mainCamera.GetComponent<AudioListener>();
|
|
if (audio != null) audio.enabled = false;
|
|
}
|
|
mainCamera = null;
|
|
}
|
|
|
|
// Called on the server when client issues the command
|
|
[Command]
|
|
void CmdSendAnimationState(bool walking, bool running)
|
|
{
|
|
// update server-side animator if present
|
|
if (animator == null) animator = GetComponent<Animator>();
|
|
if (animator != null)
|
|
{
|
|
animator.SetBool(isWalkingHash, walking);
|
|
animator.SetBool(isRunningHash, running);
|
|
}
|
|
|
|
// broadcast to all clients
|
|
RpcUpdateAnimationState(walking, running);
|
|
}
|
|
|
|
// Executed on all clients to apply animator changes
|
|
[ClientRpc]
|
|
void RpcUpdateAnimationState(bool walking, bool running)
|
|
{
|
|
if (animator == null) animator = GetComponent<Animator>();
|
|
if (animator == null) return;
|
|
|
|
animator.SetBool(isWalkingHash, walking);
|
|
animator.SetBool(isRunningHash, running);
|
|
}
|
|
}
|