using System; using System.Collections; using System.Collections.Generic; using System.Linq; using BITFALL.Player.Equip; using BITFALL.Player.Movement; using BITKit; using BITKit.Entities; using BITKit.Selection; using BITKit.StateMachine; using Unity.Mathematics; using UnityEngine; using UnityEngine.AI; using UnityEngine.InputSystem; using UnityEngine.InputSystem.Interactions; using UnityEngine.Splines; using UnityEngine.UI; namespace BITFALL.Entities.Player.Movement.States { [Serializable] public class Vault : BasicMovement, IPlayerVaultState { private Collider _collider; private bool _isHigher; private Vector3 _targetPos; private bool _inBounds; private CapsuleCollider _playerCollider; private bool _entered; private bool _exited; public override void OnStateEntry(IState old) { base.OnStateEntry(old); _entered = _exited = _inBounds = false; _playerCollider = self.GetComponent(); self.LimitViewAngle = 45; actor.ForceNotGrounded(); _collider = self.vaultPoint.Collider; actor.PhysicsComponent.IgnoreCollision(_collider.transform,true); _targetPos = self.vaultPoint.CurrentPoint; _isHigher = false; actor.alwaysNotGrounded = true; } public override void OnStateExit(IState old, IState newState) { base.OnStateExit(old, newState); //actor.RigidbodyComponent.IsKinematic = false; self.LimitViewAngle = 0; self.ExpectJump.Reset(); actor.alwaysNotGrounded = false; actor.ForceNotGrounded(); actor.PhysicsComponent.IgnoreCollision(_collider.transform,false); } public override void OnStateUpdate(float deltaTime) { base.OnStateUpdate(deltaTime); var samplePos = actor.Position; var tempPos = samplePos; tempPos.y = _collider.bounds.center.y; _inBounds = _collider.bounds.Intersects(_playerCollider.bounds) || _collider.bounds.Contains(tempPos); if (_inBounds) { actor.ForceNotGrounded(); if (actor.Position.y < _targetPos.y) { var pos = actor.Position; pos.y = _targetPos.y - 1; actor.Position = pos; _isHigher = true; } _entered = true; } else { if (_entered && _exited is false) { _exited = true; //Debug.Log("Exited"); } if (samplePos.y > _targetPos.y) { _isHigher = true; } if (_isHigher is false) { actor.Position += actor.Up * (8 * deltaTime); } } } public override void UpdateVelocity(ref Vector3 currentVelocity, float deltaTime) { if (_exited) { base.UpdateVelocity(ref currentVelocity, deltaTime); currentVelocity.y = Mathf.Clamp(currentVelocity.y, -2, 8); } else { if (currentVelocity.GetLength() < 1) { currentVelocity = actor.Forward; } currentVelocity.y = 0; } } public override void UpdateRotation(ref Quaternion currentRotation, float deltaTime) { var targetRotation = currentRotation; base.UpdateRotation(ref targetRotation, deltaTime); currentRotation = Quaternion.Lerp(currentRotation, targetRotation, deltaTime); } public override void AfterUpdateMovement(float deltaTime) { //base.AfterUpdateMovement(deltaTime); self.CurrentCameraPosition.shouldBe = self.FpvLocalPosition; } public bool ManualCancel() { if (Enabled is false) return false; Exit(); return true; } } [Serializable] public class EdgeClimb : PlayerCharacterState, IPlayerEdgeClimbState { [SerializeField] private LayerMask layerMask; public int Phase { get; set; } public float3 CurrentPoint => climbPoint; [Inject] private IEquipService _equipService; private Collider _collider; private Quaternion rotation; private readonly List _colliders = new List(); private Animator _animator; private Collider[] rangeColliders = new Collider[8]; private int rangeLength; private Vector3 handPoint; private Vector3 climbPoint; private List<(Collider collider,Vector3 closePoint)> closePoints = new List<(Collider, Vector3)>(); private Collider[] tempColliders = new Collider[16]; public float3 NextPoint { get; set; } public bool ManualCancel() { if (Enabled is false) return false; Exit(); return true; } public float3[] ClosestPoint => closePointList.ValueArray; private CacheList closePointList = new(); public override void OnStateEntry(IState old) { base.OnStateEntry(old); _animator = self.GetComponent(); _collider = self.edgeClimbPoint.Collider; _colliders.Add(_collider); climbPoint = self.edgeClimbPoint.CurrentPoint; actor.PhysicsComponent.IgnoreCollision(_collider.transform,true); var position = actor.Position+ actor.Forward * -1; position.y = _collider.bounds.center.y; var direction = _collider.bounds.center - position; direction = Vector3.ProjectOnPlane(direction, Vector3.up); var ray = new Ray(position, direction); Debug.DrawRay(ray.origin,ray.direction * 8,Color.red,1f); if (_collider.Raycast(ray, out var hit, 64)) { rotation = Quaternion.LookRotation(hit.normal) * Quaternion.Euler(0, 180, 0);; } else { BIT4Log.Warning(" Edge Climb Raycast Failed"); } actor.alwaysNotGrounded = true; actor.ColliderComponent.enabled = false; actor.UseRootMotion = true; self.LimitViewAngle = 90; Phase = 0; _equipService.AllowEquip.AddDisableElements(this); self.inputActionGroup.RegisterCallback(self.JumpAction, OnJump); self.inputActionGroup.RegisterCallback(self.MovementAction, OnMovement); } private void OnMovement(InputAction.CallbackContext obj) { // if (Phase is not (4 or 5 or 6)) return; // var value = obj.ReadValue(); // if (value is { x: 0, y: 1 }) // { // OnJump(); // } } private void OnJump(InputAction.CallbackContext obj) { if (obj is not {interaction:PressInteraction,performed:true}) return; if(Phase is not (4 or 5 or 6))return; OnJump(); } private void OnJump() { var input = self.InputVector; var startPos = climbPoint - actor.Forward * 0.1f; var allowExit = Physics.Linecast(startPos, startPos + actor.Forward * 1f + actor.Up * 0.5f, out _, layerMask) is false; if (input.y is not -1) { if( (allowExit && input.x is 0) || closePoints.Count is 0) { var length = Physics.OverlapSphereNonAlloc(climbPoint, 1f, tempColliders, layerMask); for (var i = 0; i < length; i++) { var collider = tempColliders[i]; _colliders.Add(collider); actor.PhysicsComponent.IgnoreCollision(collider.transform, true); } Phase = 8; return; } } if (closePoints.Count is 0) return; var direction = actor.Rotation * (Vector2)input; var samplePoint = climbPoint + direction; var close = closePoints.OrderBy(Close).First(); if(input.y is 1 && close.closePoint.y < climbPoint.y) return; if(input.y is -1 && close.closePoint.y > climbPoint.y) return; var offset = climbPoint-actor.Position; actor.PhysicsComponent.IgnoreCollision(close.collider.transform,true); _collider = close.collider; _colliders.Add(_collider); NextPoint = close.closePoint - offset; Phase = 1; self.Stamina -= 10; return; float Close((Collider collider, Vector3 closePoint) arg) { var distance = Vector3.Distance(climbPoint, arg.closePoint); var panel = new Plane(-actor.Forward,climbPoint); var center = panel.ClosestPointOnPlane(climbPoint+direction); var localPosition = panel.ClosestPointOnPlane(arg.closePoint)-center; var pos = new Vector2 { y = localPosition.y, x = -localPosition.z }; return Vector2.Distance(pos, default); } } public override void OnStateExit(IState old, IState newState) { base.OnStateExit(old, newState); actor.alwaysNotGrounded = false; actor.ColliderComponent.enabled = true; actor.UseRootMotion = false; self.LimitViewAngle = 0; _equipService.AllowEquip.RemoveDisableElements(this); self.inputActionGroup.UnRegisterCallback(self.JumpAction, OnJump); self.inputActionGroup.UnRegisterCallback(self.MovementAction, OnMovement); foreach (var x in _colliders) { actor.PhysicsComponent.IgnoreCollision(x.transform,false); } _colliders.Clear(); } public override void OnStateUpdate(float deltaTime) { base.OnStateUpdate(deltaTime); switch (Phase) { case 0: var targetPos = self.ExpectClimb.shouldBe - Vector3.up * 1.6f; targetPos -= self.Rotation * Vector3.forward * 0.4f; if (actor.Position.y >= targetPos.y) Phase = 5; if (Phase is 5) actor.Position = targetPos; return; case 1: if (Vector3.Distance(actor.Position, NextPoint) < 0.1f) { Phase = 5; } break; case 4 or 5 or 6 or 7 or 9: self.Stamina -= Phase switch { 4 or 6=>5, _=>1, }* deltaTime; if (self.Stamina <= 0) { Exit(); return; } handPoint = Vector3.Lerp( _animator.GetBoneTransform(HumanBodyBones.LeftHand).position, _animator.GetBoneTransform(HumanBodyBones.RightHand).position, 0.5f ); climbPoint = _collider.ClosestPoint(actor.Position + Vector3.up * 10); rangeLength = Physics.OverlapSphereNonAlloc(climbPoint, 1.6f, rangeColliders, layerMask); closePoints.Clear(); closePointList.Clear(); for (var i = 0; i < rangeLength; i++) { var collider = rangeColliders[i]; if (collider == _collider) { continue; } if (collider is MeshCollider { convex: false }) { continue; } var closestPoint = collider.ClosestPoint(actor.Position + actor.Forward * -1); closestPoint.y = collider.bounds.center.y + collider.bounds.extents.y; if (Vector3.Distance(closestPoint, climbPoint) > 2) { continue; } var length = Physics.OverlapSphereNonAlloc(closestPoint, 0.01f, tempColliders, layerMask); switch (length) { case <2: break; case 2: if (_collider == tempColliders[0] || _collider==tempColliders[1]) { break; } continue; default:continue; } var direction = closestPoint - climbPoint; direction = actor.transform.InverseTransformVector(direction); closePoints.Add((collider, closestPoint)); closePointList.Add(closestPoint); } if (_collider.Raycast(new Ray(climbPoint + actor.Forward * -0.1f + actor.Up*-0.1f + actor.Right * (self.InputVector.x * 0.2f), actor.Forward), out _, 1f) is false) { try { foreach (var x in closePoints) { if (Vector3.Distance(x.closePoint, climbPoint) < 0.2f) { _collider = x.collider; throw new InGameException(); } } Phase = 5; } catch (InGameException){} } else Phase = Phase switch { 5 when self.InputVector.x is not 0 => self.InputVector.x > 0 ? 4 : 6, 4 or 6 when self.InputVector.x is 0 => 5, _ => Phase }; var point = actor.Position; point.y = _animator.GetBoneTransform(HumanBodyBones.RightHand).position.y; point.y -= 0.2f; break; } } private float upVelocity; public override void UpdateVelocity(ref Vector3 currentVelocity, float deltaTime) { //base.UpdateVelocity(ref currentVelocity, deltaTime); switch (Phase) { case 0: var currentPosition = actor.Position; currentPosition = Vector3.Lerp( currentPosition, self.ExpectClimb.shouldBe - Vector3.up * 1.6f + rotation * Vector3.forward * -0.5f,8*deltaTime); currentPosition.y = actor.Position.y; currentPosition.y+=(upVelocity+=deltaTime)*deltaTime; if(_collider.Raycast(new Ray(currentPosition,Vector3.forward),out var fixHit,1.6f)) { currentPosition.x = fixHit.point.x; currentPosition.z = fixHit.point.y; } actor.Position = currentPosition; break; case 1: actor.Position += ((Vector3)NextPoint - actor.Position).normalized * (6 * deltaTime); break; case 4 or 5 or 6: var hand = _animator.GetBoneTransform(HumanBodyBones.LeftHand); var closePoint = _collider.ClosestPoint(actor.Position+Vector3.up*8-actor.Forward); var offset = closePoint.y - hand.position.y - 0.08f; actor.Position+=actor.Up * (offset * 8 * deltaTime); if (_collider.Raycast(new Ray(hand.position - actor.Forward * 0.4f, actor.Forward), out var hit, 1f)) { var targetFootPosition = hit.point - actor.Forward * 0.05f; rotation = Quaternion.LookRotation(hit.normal) * Quaternion.Euler(0, 180, 0); if (Vector3.Distance(targetFootPosition, hand.position) > 0.01f) { Debug.DrawLine(hand.position, hit.point, Color.red, 1f); actor.Position += (targetFootPosition - hand.position) * (deltaTime * 8); } else { Debug.DrawLine(hand.position, hit.point, Color.green, 1f); } } break; } switch (Phase) { case 0: currentVelocity = Vector3.up; break; } } public override void UpdateRotation(ref Quaternion currentRotation, float deltaTime) { if (Phase is 0 or 4 or 5 or 6) { currentRotation = rotation; } } public override void AfterUpdateMovement(float deltaTime) { self.CurrentCameraPosition.shouldBe = self.FpvLocalPosition; } public override void DrawGizmos() { Gizmos.DrawWireCube(climbPoint,Vector3.one*0.1f); Gizmos.DrawCube(handPoint,Vector3.one*0.1f); Gizmos.color=Color.green; foreach (var x in closePoints) { Gizmos.DrawSphere(x.closePoint,0.1f); } } } [Serializable] public class Climb : PlayerCharacterState,IPlayerClimbState { [SerializeField] protected float lerpDelta = 5; private IntervalUpdate cancelInterval = new(0.16f); private Transform ignore; private bool needInit; private float _startDistance; private Vector3 _startPos; public override void OnStateEntry(IState old) { ignore = self.climbClosePoint.Collider.transform; actor.PhysicsComponent.IgnoreCollision(ignore,true); base.OnStateEntry(old); self.ExpectCrouch.Reset(); actor.alwaysNotGrounded = true; actor.ForceNotGrounded(); actor.ColliderComponent.enabled = false; cancelInterval.Reset(); needInit = true; self.LimitViewAngle = 80; _startDistance = Vector3.Distance(actor.Position, self.ExpectClimb); _startPos = actor.Position; } public override void OnStateUpdate(float deltaTime) { if (cancelInterval.AllowUpdate && actor.Velocity.sqrMagnitude<=0.16f || Vector3.Distance(self.ExpectClimb,_startPos) > _startDistance && actor.Position.y > self.ExpectClimb.shouldBe.y ) { Exit(); return; } var direction = self.transform.InverseTransformPoint(self.ExpectClimb.shouldBe); if (direction.z <= -0.3f) { Exit(); return; } // var pos = actor.Position; // pos.y = self.ExpectClimb.shouldBe.y; // if (self.Position.y >= self.ExpectClimb.shouldBe.y && Vector3.Distance(pos,self.ExpectClimb.shouldBe)<0.1f) // { // Exit(); // } } public override void OnStateExit(IState old, IState newState) { actor.PhysicsComponent.IgnoreCollision(ignore,false); actor.alwaysNotGrounded = false; actor.ColliderComponent.enabled = true; self.ExpectJump.Reset(); actor.ForceGrounded(); self.LimitViewAngle = 0; // } public override void UpdateVelocity(ref Vector3 currentVelocity, float deltaTime) { if (needInit) { currentVelocity.y = 0; needInit = false; } var velocity =(self.ExpectClimb.shouldBe - actor.Position)*lerpDelta; velocity += self.Rotation * self.MovementInput.normalized * 1.0f; currentVelocity = Vector3.Lerp(currentVelocity,velocity,lerpDelta*deltaTime); } public override void AfterUpdateMovement(float deltaTime) { self.CurrentCameraPosition.shouldBe = self.FpvLocalPosition; } public bool ManualCancel() { if (!Enabled) return false; Exit(); return true; } } [Serializable] public sealed class Link:PlayerCharacterState,IPlayerLinkState { public int LinkArea { get; private set; } private OffMeshLink _offMeshLink; private ISplineContainer _splineContainer; private float progress; private float increment; [Inject] private IEquipService _equipService; [Inject] private InputActionGroup _inputActionGroup; private readonly IntervalUpdate exitInterval = new(0.32f); public override void OnStateEntry(IState old) { base.OnStateEntry(old); self.ExpectCrouch.Reset(); self.ExpectJump.Reset(); actor.ForceNotGrounded(); actor.RigidbodyComponent.IsKinematic = true; actor.ColliderComponent.enabled = false; actor.UseRootMotion = true; Init(self.OffMeshLink); self.LimitViewAngle = 45; _equipService.AllowEquip.AddDisableElements(this); exitInterval.Reset(); _inputActionGroup.RegisterCallback(self.CrouchAction, OnCrouch); _inputActionGroup.RegisterCallback(self.JumpAction, OnJump); } private void OnJump(InputAction.CallbackContext obj) { switch (obj) { case {interaction:PressInteraction,performed:true}: Exit(); return; } } private void OnCrouch(InputAction.CallbackContext obj) { switch (obj) { case { interaction: PressInteraction, performed: true }: Exit(); return; } } public override void OnStateExit(IState old, IState newState) { base.OnStateExit(old, newState); actor.RigidbodyComponent.IsKinematic = false; actor.ColliderComponent.enabled = true; actor.UseRootMotion = false; self.LimitViewAngle = 0; _equipService.AllowEquip.RemoveDisableElements(this); _inputActionGroup.UnRegisterCallback(self.CrouchAction, OnCrouch); _inputActionGroup.UnRegisterCallback(self.JumpAction, OnJump); } private void Init(OffMeshLink offMeshLink) { var offsetTransform = offMeshLink.transform; _offMeshLink = offMeshLink; offMeshLink.TryGetComponent(out _splineContainer); LinkArea = offMeshLink.area; if (_splineContainer is not null) { var isStart = Vector3.Distance( actor.Position, offsetTransform.TransformPoint(_splineContainer.Splines[0].Knots.First().Position) ) < Vector3.Distance( actor.Position, offsetTransform.TransformPoint(_splineContainer.Splines[0].Knots.Last().Position) ); progress = isStart ? 0 : 1; increment = isStart ? 1 : -1; } else { var offset = Mathf.Lerp(offMeshLink.startTransform.position.y,offMeshLink.endTransform.position.y,0.5f); var offsetPos = actor.Position; offsetPos.y = offset; actor.ForceNotGrounded(); actor.Position = Vector3.MoveTowards(actor.Position, offsetPos, 0.64f); progress = 0.5f; } } public override void UpdateRotation(ref Quaternion currentRotation, float deltaTime) { if (_splineContainer is null) { actor.Rotation = Quaternion.Lerp( actor.Rotation, _offMeshLink.transform.rotation * Quaternion.Euler(0,-180,0), 5 * deltaTime ); } } public override void UpdateVelocity(ref Vector3 currentVelocity, float deltaTime) { actor.ForceNotGrounded(); if (_offMeshLink is null) return; switch (_splineContainer) { case null: var positionA = _offMeshLink.startTransform.position; var positionB = positionA; positionB.y = _offMeshLink.endTransform.position.y; var positionC = actor.Position + Vector3.up * (0 * deltaTime * 1.6f); Vector3 vectorAC = positionC - positionA; Vector3 vectorAB = positionB - positionA; Vector3 closestPoint = positionA + Vector3.Project(vectorAC, vectorAB); actor.Position = closestPoint; //currentVelocity = Vector3.Lerp(currentVelocity , (closestPoint - actor.Position) , 5 * deltaTime); self.UnityEntity.SetDirect(BITHash.Player.ActionSpeed,self.MovementInput.z); break; case not null: var trans = _offMeshLink.transform; var pos =trans.position + trans.rotation * _splineContainer.Splines[0].EvaluatePosition(progress +=increment * deltaTime); actor.Position =Vector3.Lerp(actor.Position , pos , 8*deltaTime) ; break; } } public override void OnStateUpdate(float deltaTime) { if (exitInterval.AllowUpdateWithoutReset is false) return; switch (_offMeshLink,_splineContainer) { case (null,null): case (not null,_) when !_offMeshLink || _offMeshLink.gameObject.activeInHierarchy is false: Exit(); return; case (not null,not null): if (progress is > 1.1f or < -0.1f) { Exit(); } return; case (not null,null): if (actor.Position.y > _offMeshLink.endTransform.position.y - 1) { Exit(); actor.Position = _offMeshLink.endTransform.position; return; } if (actor.Position.y < _offMeshLink.startTransform.position.y) { Exit(); actor.Position = _offMeshLink.startTransform.position; return; } break; } } public override void AfterUpdateMovement(float deltaTime) { self.CurrentCameraPosition.shouldBe = self.FpvLocalPosition; } } [Serializable] public sealed class Dodge : PlayerCharacterState,IPlayerDodgeState { [SerializeField] private int costStamina = 10; [SerializeField] private AnimationCurve curve; [SerializeField] private float duration = 0.32f; [Inject] private IEquipService _equipService; private int direction; private float process; [Inject] private IHealth _health; public override void Initialize() { base.Initialize(); _health.OnDamageFactory += OnDamageFactory; } public override void OnStateEntry(IState old) { base.OnStateEntry(old); direction = self.MovementInput.x > 0 ? 1 : -1; process = 0; self.Stamina-= costStamina; actor.UseRootMotion = true; _equipService.AllowEquip.AddDisableElements(this); } public override void OnStateExit(IState old, IState newState) { base.OnStateExit(old, newState); _equipService.AllowEquip.RemoveDisableElements(this); actor.UseRootMotion = false; } public override void OnStateUpdate(float deltaTime) { base.OnStateUpdate(deltaTime); process += deltaTime / duration; } // public override void UpdateVelocity(ref Vector3 currentVelocity, float deltaTime) // { // currentVelocity = actor.transform.right * (direction * curve.Evaluate(process+=deltaTime / duration)); // } public override void AfterUpdateMovement(float deltaTime) { if(process>=1) Exit(); if(actor.IsGrounded is false)Exit(); } private int OnDamageFactory(DamageMessage msg, int currentDamage) { if (self.CurrentState is not IPlayerDodgeState || msg.Initiator is not Entity initiator) return currentDamage; var _direction = self.Position - initiator.transform.position; var verticalAngle = Vector3.Angle(initiator.transform.forward, _direction) - 90.0f; //Debug.Log(verticalAngle); return 0; } public bool ManualCancel() { if (!Enabled) return false; Exit(); return true; } } [Serializable] public sealed class Fixed:PlayerCharacterState,IPlayerFixedState { public object FixedObject=>_fixedPlace; public float3 FixedPosition=>_fixedPlace.FixedPosition; public quaternion FixedRotation=>_fixedPlace.FixedRotation; public float3 FixedLocalPosition => throw new NotImplementedException(); public quaternion FixedLocalRotation => throw new NotImplementedException(); private IPlayerFixedPlace _fixedPlace; [Inject] private ISelector _selector; [Inject] private IEquipService _equipService; [Inject] private IKnockdown _knockdown; public override void Initialize() { base.Initialize(); _selector.OnActive+=OnSelectorActive; } public override void OnStateEntry(IState old) { base.OnStateEntry(old); //actor.enabled = false; //actor.RigidbodyComponent.IsKinematic = true; actor.ColliderComponent.enabled = false; actor.PhysicsComponent.enabled = false; actor.alwaysNotGrounded = true; actor.constraintRotation = false; self.ExpectCrouch.Reset(); self.LimitViewAngle = 100; _equipService.AllowEquip.AddDisableElements(this); lastRotation = _fixedPlace.FixedRotation; } public override void OnStateExit(IState old, IState newState) { base.OnStateExit(old, newState); //actor.RigidbodyComponent.IsKinematic = false; actor.ColliderComponent.enabled = true; actor.alwaysNotGrounded = false; actor.PhysicsComponent.enabled = true; actor.constraintRotation = true; //actor.enabled = true; self.LimitViewAngle = 0; _equipService.AllowEquip.RemoveDisableElements(this); } public override void UpdateVelocity(ref Vector3 currentVelocity, float deltaTime) { currentVelocity = default; actor.Position = FixedPosition + _fixedPlace.Velocity * deltaTime; ; } public override void UpdateRotation(ref Quaternion currentRotation, float deltaTime) { currentRotation = FixedRotation; } public override void AfterUpdateMovement(float deltaTime) { base.AfterUpdateMovement(deltaTime); self.CurrentCameraPosition.shouldBe = self.FpvLocalPosition; if (self.ExpectCrouch.shouldBe && _fixedPlace.Exit(self.Entity)) { self.ExpectCrouch.Reset(); Exit(); }else if (_fixedPlace is null) { Exit(); } } private void OnSelectorActive(ISelectable obj) { if (obj is not MonoBehaviour monoBehaviour || monoBehaviour.TryGetComponent(out var fixedPlace) is false) return; if (_knockdown.IsKnockdown) return; if (_fixedPlace?.Exit(self.Entity) is false) { return; } if (fixedPlace.Entry(self.Entity)) { _fixedPlace = fixedPlace; self.TransitionState(); return; } _fixedPlace = null; } private quaternion lastRotation; public override void OnStateUpdate(float deltaTime) { base.OnStateUpdate(deltaTime); var rotation = Quaternion.Inverse(_fixedPlace.FixedRotation) * lastRotation; var euler =MathV.TransientRotationAxis(rotation.eulerAngles); var add = new float2(euler.x, -euler.y); self.AddViewEuler(add); lastRotation = _fixedPlace.FixedRotation; } protected override void Exit() { base.Exit(); if (_fixedPlace is not null) { actor.Position = _fixedPlace.ExitPosition; } _fixedPlace = null; } } [Serializable] public sealed class ClimbBar { } }