using System; using System.Collections; using System.Collections.Generic; using System.Data; using System.Globalization; using System.Linq; using BITKit; using BITKit.Entities; using BITKit.Physics; using BITKit.UX; using BITKit.WorldNode; using Cinemachine; using Kinemation.MotionWarping.Runtime.Core; using Lightbug.CharacterControllerPro.Core; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Net.Project.B.Health; using Net.Project.B.Interaction; using Net.Project.B.WorldNode; using NodeCanvas.Framework; using ParadoxNotion; using Project.B.Player; using Unity.AI.Navigation; using Unity.Collections.LowLevel.Unsafe; using Unity.Mathematics; using UnityEngine; using UnityEngine.AI; using UnityEngine.InputSystem; using UnityEngine.InputSystem.Controls; using UnityEngine.InputSystem.Interactions; namespace Project.B.CharacterController { public class PlayerCharacterController : ICharacterController, IPlayerCharacterController, IDisposable { private readonly ILogger _logger; internal readonly MotionWarping MotionWarping; internal readonly IEntitiesService EntitiesService; internal readonly Transform Transform; internal readonly IEntity Entity; internal readonly ICharacterService CharacterService; internal readonly CharacterActor CharacterActor; internal readonly CinemachineVirtualCameraBase VirtualCamera; internal readonly Transform PlayerView; internal readonly Transform PlayerAnimationView; internal readonly InputActionGroup InputActionGroup; internal readonly IPlayerKeyMap KeyMap; internal readonly IWrapper PlayerSettings; internal readonly IMainTicker MainTicker; internal readonly IAfterTicker AfterTicker; internal readonly IClosePointProvider StepUpPointProvider; internal readonly IClosePointProvider ClimbPointProvider; internal readonly IHealthService HealthService; internal readonly IKnockedService KnockedService; internal readonly IWorldInteractionService WorldInteractionService; internal readonly IUXService UXService; public PlayerCharacterController(Transform transform, IEntity entity, ICharacterService characterService, CharacterActor characterActor, CinemachineVirtualCameraBase virtualCamera, IPlayerKeyMap keyMap, IWrapper playerSettings, IAfterTicker afterTicker, IBlackboard blackboard, InputActionGroup inputActionGroup, IMainTicker mainTicker, IHealthService healthService, IKnockedService knockedService, ILogger logger, IWorldInteractionService worldInteractionService, IUXService uxService, IEntitiesService entitiesService, MotionWarping motionWarping) { Transform = transform; Entity = entity; CharacterService = characterService; CharacterActor = characterActor; VirtualCamera = virtualCamera; KeyMap = keyMap; PlayerSettings = playerSettings; AfterTicker = afterTicker; InputActionGroup = inputActionGroup; MainTicker = mainTicker; HealthService = healthService; KnockedService = knockedService; _logger = logger; WorldInteractionService = worldInteractionService; UXService = uxService; EntitiesService = entitiesService; MotionWarping = motionWarping; PlayerView = blackboard.GetVariable("player_view").value; PlayerAnimationView = blackboard.GetVariable("player_model_view").value; foreach (var x in Transform.GetComponentsInChildren()) { x.updateWhenOffscreen = true; } StepUpPointProvider = new GetClosePointFromCollider(blackboard.GetVariable("player_vault_point").value) { LayerMask = characterActor.stableLayerMask, MinHeight = 0.1f, MaxHeight = 1.2f, Distance = 1f, }; ClimbPointProvider = new GetClosePointFromCollider(blackboard.GetVariable("player_climb_point").value) { LayerMask = characterActor.stableLayerMask, MinHeight = 0f, MaxHeight = 0.4f }; Initialize(); AllowRun.AddElement(this); AllowSprint.AddElement(this); KnockedService.OnKnocked += OnKnocked; HealthService.OnHealthChanged += OnHealthChanged; WorldInteractionService.OnInteraction += OnInteraction; ForceRootMotion.AddListener(x => { CharacterActor.UseRootMotion = x; }); foreach (var x in Transform.GetComponentsInChildren()) { CharacterActor.PhysicsComponent.IgnoreCollision(x.transform,true); } DisableCollision.AddListener(x => { characterActor.ColliderComponent.enabled = !x; characterActor.rootMotionVelocityType = x ? PhysicsActor.RootMotionVelocityType.SetVelocity : PhysicsActor.RootMotionVelocityType.SetPlanarVelocity; }); AllowOverride.AddListener(x => { DisableCollision.SetElements(AllowOverride,x); DisableStateTransition.SetElements(AllowOverride,x); CharacterActor.enabled = !x; CharacterActor.IsKinematic = x; }); } private void OnKnocked(int arg1, bool arg2) { if (arg1 != Entity.Id) return; if (arg2) { TransitionState(); } } private void OnHealthChanged(int arg1, int arg2, int arg3, object arg4) { if(arg1!=Entity.Id)return; if (CurrentState is null or ICharacterKnocked && arg3 >= 0 && KnockedService.Knocked.Contains(Entity.Id) is false) { TransitionState(); } else if (arg3 < 0) { DisposeState(); } } public bool Enabled { get; set; } public ICharacterState CurrentState { get; set; } public event Action OnStateChanged; public event Action OnStateRegistered; public event Action OnStateUnRegistered; public IDictionary StateDictionary { get; } = new Dictionary(); internal ICharacterStateData CurrentStateData => CurrentState as ICharacterStateData; internal float2 InputDirection { get; private set; } public float2 InputView { get; set; } internal float3 CurrentCameraLocalPosition; internal bool RequestJump { get; set; } internal bool JumpedThisTime { get; set; } internal bool RequestedCrouch { get; set; } private bool _inWaterThisFrame; private readonly DoubleBuffer _fallGravity=new(); /// /// 与其同步的碰撞体,通常是翻越,攀爬与跟随运动等 /// internal Collider SyncCollider { get; set; } internal Vector3 SyncPosition{ get; set; } internal IList LimitedViewAngle { get; private set; } = new List(); private quaternion _lastOriginalRotation = quaternion.identity; private float2 _newView; private bool _clearView; private bool _shouldRun; private bool _requestRun; private readonly IntervalUpdate _disableLadder = new(0.32f); private readonly HashSet _ignoreTouchId = new(); private void OnAfterTick(float delta) { UpdateRotationInternal(delta); } private void UpdateRotationInternal(float delta) { var lastView = InputView; foreach (var func in OnInputViewFactor.CastAsFunc()) { _newView = func.Invoke(_newView); } InputView += _newView; var view = InputView; view.y = math.clamp(InputView.y, -80, 80); InputView = view; var angularVelocity = InputView - lastView; AngularVelocity = new(angularVelocity.x, angularVelocity.y, 0); if (CurrentState is null) return; quaternion viewRot = _lastOriginalRotation; quaternion rot = CharacterActor.Rotation; CurrentState?.UpdateRotation(ref CurrentCameraLocalPosition, ref rot, ref viewRot, delta); ViewPosition = Transform.position + Transform.rotation * CurrentCameraLocalPosition; //ViewRotation = Quaternion.Euler(InputView.y, InputView.x, 0); if (ForceRootMotion.Allow) { } else { Rotation = rot; } { var startPos = CharacterActor.Position + Vector3.up * CurrentCameraLocalPosition.y; var newCameraPosition = (Vector3)Position + CharacterActor.Rotation * CurrentCameraLocalPosition; switch (CurrentState) { case not ICharacterSeating or ICharacterStateAnimation when Physics.Linecast(startPos,startPos+CharacterActor.Forward*CurrentCameraLocalPosition.z, out var hit, CharacterActor.stableLayerMask) && hit.collider.isTrigger is false: //var newPos = startPos + (Quaternion)ViewRotation * Vector3.forward * (hit.distance - 0.04f); //Debug.Log(hit.distance); var newPos = CharacterActor.transform.InverseTransformPoint(Vector3.MoveTowards( hit.point,startPos,0.04f)); PlayerView.transform.localPosition = newPos; DisableModel.AddElement(1); break; default: PlayerView.transform.localPosition = CurrentCameraLocalPosition; DisableModel.RemoveElement(1); break; } } //AdditiveCameraQuaternion.Set(0,_playerAnimationView.localRotation); Quaternion finalRot = viewRot; foreach (var (index, value) in AdditiveCameraQuaternion) { finalRot *= value; } PlayerView.transform.rotation = finalRot; if (_clearView) _newView = default; _lastOriginalRotation = finalRot; } public void Initialize() { CharacterService.Dictionary.Add(Entity.Id, this); AfterTicker.Add(OnAfterTick); MainTicker.Add(OnTick); VirtualCamera.enabled = true; InputActionGroup.RegisterCallback(KeyMap.MovementKey, OnMovement); InputActionGroup.RegisterCallback(KeyMap.ViewKey, OnView); InputActionGroup.RegisterCallback(KeyMap.JumpKey, OnJump); InputActionGroup.RegisterCallback(KeyMap.CrouchKey, OnCrouched); InputActionGroup.RegisterCallback(KeyMap.RunKey, OnRun); InputActionGroup.allowInput.AddElement(this); } private void OnTick(float obj) { if (BITInputSystem.AllowInput.Allow is false) { InputDirection = default; _newView = default; } if (Touchscreen.current is { wasUpdatedThisFrame: true }) { _clearView = true; } if (Touchscreen.current is not null) { foreach (var touchControl in Touchscreen.current.touches) { var touchId = touchControl.touchId.value; if (touchControl.press.isPressed is false) { _ignoreTouchId.Remove(touchId); continue; } if (UXService.TryPick(touchControl.startPosition.ReadValue(), out var ve)) { _ignoreTouchId.Add(touchId); continue; } if(_ignoreTouchId.Contains(touchId))continue; var delta = touchControl.delta.ReadValue() * PlayerSettings.Value.TouchSensitivity; delta.y = -delta.y; _newView += (float2)delta; _newView.y = math.clamp(_newView.y, -80, 80); } } } private void OnRun(InputAction.CallbackContext obj) { _requestRun = obj.JustPressed(); if (AllowTpsCamera.Allow) { if (MathV.GetLength(InputDirection) is 0) return; } else { if (InputDirection.y <= 0 && SelfVelocity.z <= 0) return; } switch (obj) { case { interaction: MultiTapInteraction, performed: true }: case { interaction: PressInteraction, performed: true }: switch (CurrentState) { case ICharacterKnocked: break; case ICharacterStateWalk when _shouldRun && Stamina > 8: TransitionState(); break; default: if (CurrentState is ICharacterStateRun && Stamina > 8) { TransitionState(); } _shouldRun = true; break; } break; } } private void OnCrouched(InputAction.CallbackContext obj) { if (obj is not { interaction: PressInteraction, performed: true }) return; if (CurrentState is ICharacterParachute) { TransitionState(); return; } switch (CurrentState) { case ICharacterStateStepUp: case ICharacterStateClimb: case ICharacterSeating: _shouldRun = false; TransitionState(); return; case ICharacterStateRun: case ICharacterSprint when Stamina>5: _shouldRun = false; TransitionState(); Stamina -= 30; return; case ICharacterKnocked: return; case ICharacterStateCrouched: if (CharacterActor.CheckSize(CharacterActor.DefaultBodySize)) { TransitionState(); } return; default: _shouldRun = false; RequestedCrouch = true; TransitionState(); return; } } private void OnJump(InputAction.CallbackContext obj) { if (obj.JustPressed()) { if (Velocity.y < -8) { switch (CurrentState) { case ICharacterFreeFall: { TransitionState(); } return; } } } switch (CurrentState) { case ICharacterKnocked: RequestJump = false; return; } RequestJump = obj switch { { interaction: PressInteraction, performed: true } => true, { interaction: PressInteraction, canceled: true } => false, _ => RequestJump }; if (obj is { interaction: PressInteraction }) { if (JumpedThisTime && RequestJump) { JumpedThisTime = false; } } } private bool _isTouching; private void OnView(InputAction.CallbackContext obj) { if (Touchscreen.current is null) { if (BITAppForUnity.AllowCursor.Allow) return; } if (Touchscreen.current is { touches: { Count: > 0 } }) return; _newView = (float2)obj.ReadValue() * (obj.control.device) switch { Gamepad => PlayerSettings.Value.GamePadSensitivity, _ when obj.control.usages.ToString() is "OnScreen" => PlayerSettings.Value.TouchSensitivity, _ when Touchscreen.current is { touches: { Count: > 0 } } => PlayerSettings.Value.TouchSensitivity, Touchscreen => PlayerSettings.Value.TouchSensitivity, Mouse => PlayerSettings.Value.Sensitivity * 0.022f, _ => PlayerSettings.Value.Sensitivity }; _clearView = obj.control.device switch { Gamepad => false, _ => true, }; _newView.y = -_newView.y; } private Vector2 _touchscreenStartPos; private void OnMovement(InputAction.CallbackContext obj) { if (_requestRun) { _shouldRun = true; } InputDirection = obj.ReadValue(); MoveInput = new float3(InputDirection.x,0, InputDirection.y); switch (InputDirection) { case {y : 0,x: 0} when AllowTpsCamera.Allow: case { y: <= 0 } when AllowTpsCamera.Allow is false: _shouldRun = false; switch (CurrentState) { case ICharacterStateRun: case ICharacterSprint: TransitionState(); break; } break; } } public void UpdateState(float deltaTime) { if (CharacterActor.IsFalling && CharacterActor.IsGrounded is false) { _fallGravity.Release(Velocity); } else { if (_fallGravity.TryGetRelease(out var fall)) { OnLand?.Invoke(fall,0); switch (CurrentState) { case ICharacterKnocked: case ICharacterFreeFall: { HealthService.AddHealth(Entity.Id, -101, this); } break; } } } if (_refreshStamina.AllowUpdateWithoutReset) { _floatStamina += 32 * deltaTime; if (_floatStamina > 0) { var addStamina = (int)_floatStamina; Stamina += addStamina; _floatStamina -= addStamina; } } switch (CurrentState) { case ICharacterStateCrouched: case ICharacterSliding: if (RequestJump) { if (CharacterActor.CheckSize(CharacterActor.DefaultBodySize)) { RequestJump = CurrentState is ICharacterSliding; TransitionState(); } } break; } if (DisableStateTransition.Allow) { } else { var isSizeGood = CharacterActor.CheckSize(CharacterActor.DefaultBodySize); switch (CurrentState) { case ICharacterStateCrouched when RequestedCrouch is false && isSizeGood: { TransitionState(); } break; case ICharacterStateWalk or ICharacterStateRun when isSizeGood is false: TransitionState(); break; case ICharacterStateWalk or ICharacterStateIdle or ICharacterStateCrouched or ICharacterSliding when _shouldRun && AllowRun.Allow: if (isSizeGood) { if (AllowTpsCamera.Allow || InputDirection.y > 0) TransitionState(); } break; case ICharacterSprint: case ICharacterStateRun: switch (AllowRun.Allow, AllowTpsCamera.Allow, InputDirection) { case (false, _, _): case (_, true, { x: 0, y: 0 }): case (_, false, { y: <= 0 }): TransitionState(); break; } break; } } if (SelfVelocity.y < -20) { switch (CurrentState) { case ICharacterStateIdle: case ICharacterStateWalk: case ICharacterStateRun: case ICharacterSprint: case ICharacterStateCrouched: { TransitionState(); } return; } } switch (CurrentState) { case ICharacterStateIdle: case ICharacterStateWalk: case ICharacterStateRun: case ICharacterSprint: case ICharacterStateClimb: if (RequestJump) { if (StepUpPointProvider.TryGetValue(out var nextPosition, out var nextCollider)) { SyncCollider = nextCollider; SyncPosition = nextPosition; try { var startPos = SyncPosition + CharacterActor.Rotation * new Vector3(0,-0.2f,-0.1f); if (nextCollider.Raycast(new Ray(startPos, SyncPosition - startPos), out var hit, int.MaxValue)) { if (nextCollider.Raycast(new Ray(startPos + hit.normal * -2, hit.normal), out hit, int.MaxValue)) { foreach (var collider in Physics.OverlapSphere(hit.point, 0.2f, CharacterActor.stableLayerMask)) { if (collider == SyncCollider) continue; TransitionState(); throw new OperationCanceledException(); } if (Vector3.Distance(hit.point, SyncPosition) < 0.5f) { if(CurrentState is ICharacterStateClimb) { if ( /*Physics.Raycast(hit.point + hit.normal * -0.1f, Vector3.down, out _, 2f, CharacterActor.stableLayerMask) || */ MoveInput.z>0) { TransitionState(); throw new OperationCanceledException(); } TransitionState(); throw new OperationCanceledException(); } TransitionState(); throw new OperationCanceledException(); } } } TransitionState(); } catch (OperationCanceledException) { } return; } if (CurrentState is ICharacterStateClimb) { JumpedThisTime = true; TransitionState(); CharacterActor.ForceNotGrounded(); CharacterActor.Position -= CharacterActor.Forward * 0.4f; CharacterActor.Velocity += (CharacterActor.GroundContactNormal + CharacterActor.Up + CharacterActor.Forward) .normalized * 8 + CharacterActor.Forward * 8f; return; } if (CurrentState is not ICharacterStateClimb) { if (ClimbPointProvider.TryGetValue(out nextPosition, out nextCollider)) { SyncCollider = nextCollider; SyncPosition = nextPosition; TransitionState(); RequestJump = false; return; } } } break; } CurrentState?.OnStateUpdate(deltaTime); float3 currentVelocity = CharacterActor.Velocity; CurrentState?.BeforeUpdateVelocity(deltaTime); CurrentState?.UpdateVelocity(ref currentVelocity, deltaTime); CurrentState?.AfterUpdateVelocity(deltaTime); ForceRootMotion.SetElements(8961844,CurrentState is null); if (CurrentState is not null && CharacterActor.RigidbodyComponent.IsKinematic is false && AllowOverride.Allow is false) { CharacterActor.Velocity = currentVelocity; } switch (CurrentState) { case ICharacterStateWalk: case ICharacterStateRun: case ICharacterSprint: case ICharacterStateCrouched: case ICharacterFreeFall: case ICharacterSwimming: { _inWaterThisFrame = false; foreach (var trigger in CharacterActor.Triggers) { if(!trigger.gameObject)continue; if (trigger.gameObject.layer == LayerMask.NameToLayer("Water")) { _inWaterThisFrame = true; if (CurrentState is not ICharacterSwimming) { SyncCollider = trigger.collider3D; TransitionState(); break; } } if (_disableLadder.AllowUpdateWithoutReset) { if (trigger.transform.TryGetComponent(out var link) && link.area == NavMesh.GetAreaFromName("Ladder")) { var ladder = Entity.ServiceProvider.GetRequiredService(); ladder.UpPoint = link.transform.TransformPoint(link.endPoint); ladder.DownPoint = link.transform.TransformPoint(link.startPoint); SyncCollider = link.GetComponent(); if (Position.y < ladder.UpPoint.y - 0.5f) { TransitionState(ladder); } _disableLadder.Reset(); break; } } } } break; } if (CurrentState is ICharacterSwimming && _inWaterThisFrame is false) { TransitionState(); } } private float _floatStamina; public void DisposeState() { CharacterActor.Velocity = default; TransitionState(null); } public ICharacterState TransitionState() where TState : ICharacterState { //var state = StateDictionary[typeof(TState)]; if (_isDisposed) return null; var state = Entity.ServiceProvider.GetRequiredService(); StateDictionary.TryAdd(typeof(TState), state); if (state == null || Equals(CurrentState, state)) return state; TransitionState(state); return state; } public ICharacterState TransitionState(ICharacterState state) { if (Equals(CurrentState, state)) return state; if (CurrentState is ICharacterStateClimb || state is ICharacterStateClimb) { _disableLadder.Reset(); } if (state is not ICharacterStateCrouched or ICharacterSliding) { RequestedCrouch = false; } CurrentState?.OnStateExit(CurrentState, state); if (state is not null) { //_characterActor.IsKinematic = false; CharacterActor.ColliderComponent.enabled = true; var data = state as ICharacterStateData; var size = CharacterActor.BodySize; size.y = data!.BaseHeight; CharacterActor.SetSize(size, CharacterActor.SizeReferenceType.Bottom); } else { //_characterActor.IsKinematic = true; CharacterActor.ColliderComponent.enabled = false; } state?.OnStateEntry(CurrentState); OnStateChanged?.Invoke(CurrentState, state); CurrentState = state; AllowMovement.SetElements(this,state is not null); return state; } public ValidHandle AllowMovement { get; } = new(); public float Height =>CurrentStateData?.BaseHeight ?? 1f; public float3 Center => (Vector3)Position + Vector3.up; public float3 Position { get => CharacterActor.Position; set => CharacterActor.Position = value; } public float3 Velocity { get => CharacterActor.Velocity; set { if (value.y > 0) { CharacterActor.ForceNotGrounded(); } CharacterActor.Velocity = value; } } public float3 ViewPosition { get; set; } public float3 AngularVelocity { get; private set; } public quaternion Rotation { get => CharacterActor.Rotation; set => CharacterActor.Rotation = value; } public quaternion ViewRotation { get=> Quaternion.Euler(InputView.y, InputView.x, 0); set { var eulerAngles = ((Quaternion)value).eulerAngles; InputView = new float2(eulerAngles.y,eulerAngles.x); _newView = default; } } public float3 SelfVelocity { get => CharacterActor.transform.InverseTransformDirection(Velocity); set => Velocity = CharacterActor.transform.TransformDirection(value); } public bool IsGrounded => CharacterActor.IsGrounded; public event Action OnLand; private bool _isDisposed; public void Dispose() { if(_isDisposed)return; _isDisposed = true; CharacterService.Dictionary.Remove(Entity.Id); AfterTicker.Remove(OnAfterTick); MainTicker.Remove(OnTick); CurrentState?.OnStateExit(CurrentState,null); HealthService.OnHealthChanged -= OnHealthChanged; KnockedService.OnKnocked -= OnKnocked; WorldInteractionService.OnInteraction -= OnInteraction; } private void OnInteraction(object arg1, IWorldInteractable arg2, WorldInteractionProcess arg3, object arg4) { if (arg3 is not WorldInteractionProcess.Performed) return; if(arg2.WorldObject is not GameObject gameObject)return; switch (CurrentState) { case ICharacterKnocked: case ICharacterStateEmpty: return; } if (EntitiesService.Entities.TryGetValue(gameObject.GetInstanceID(),out var entity) is false || entity.ServiceProvider.GetService() is not { } seatNode) return; var seatState = Entity.ServiceProvider.GetRequiredService(); seatState.SeatNode = seatNode; seatState.SeatEntity = entity; TransitionState(); } public float3 MoveInput { get; private set; } public event Func OnInputViewFactor; public ICharacterController CharacterController => this; public IDictionary AdditiveCameraQuaternion { get; set; } = new Dictionary(); public IDictionary ZoomFactor { get; set; } = new Dictionary() { { 0, 1 } }; public ValidHandle AllowTpsCamera { get; } = new(); public ValidHandle AllowRun { get; } = new(); public ValidHandle AllowSprint { get; } = new(); public ValidHandle DisableModel { get; } = new(); public ValidHandle ForceRootMotion { get; } = new(); public ValidHandle DisableStateTransition { get; } = new(); public ValidHandle DisableCollision { get; } = new(); public ValidHandle AllowOverride { get; } = new(); public void CancelRun() { } public void CancelSprint() { } public int Stamina { get => _stamina; set { var current = _stamina; if (value < current) { _refreshStamina.Reset(); } _stamina = math.clamp(value, 0, 100); OnStaminaChanged?.Invoke(current,_stamina); } } private int _stamina=100; public event Action OnStaminaChanged; private readonly IntervalUpdate _refreshStamina = new(1f); } }