using CSNetwork.Protocols; using CSNetwork.Protocols.RPCData; using System.Text; using TMPro; using UnityEngine; using UnityEngine.InputSystem; using UnityEngine.UI; public class CharacterCtrl : MonoBehaviour { [SerializeField] private TextMeshPro txtName; [SerializeField] private CharacterController controller; [SerializeField] private Animator animator; [SerializeField] private Joystick joystick; [SerializeField] private Button btnJump; [SerializeField] private Button btnRun; PlayerStateMachine playerStateMachine; PlayerMoveState moveState; float playerSpeed = 5.0f; float jumpHeight = 1.5f; float gravityValue = -9.81f; StateAnim stateAnim = StateAnim.Idle; Vector3 playerVelocity; [SerializeField] bool isGrounded = false; bool isRun = false; // ====== Ground cast config ====== [Header("Ground Cast")] [Tooltip("Khoảng thêm ngoài skinWidth để SphereCast xuống (m ngắn)")] [SerializeField] private float extraGroundDistance = 0.05f; [Tooltip("Bớt bán kính một chút để tránh tự va vào capsule (epsilon)")] [SerializeField] private float radiusEpsilon = 0.005f; [Tooltip("Layer mặt đất")] [SerializeField] private LayerMask groundMask; [Tooltip("Layer mặt đất")] [SerializeField] private float slopeToleranceDeg = 2f; // cache tùy chọn (không bắt buộc) float ccRadius, ccSkin; RaycastHit lastGroundHit; private void Awake() { moveState = new PlayerMoveState(this); playerStateMachine = new PlayerStateMachine(); // Cache: không bắt buộc, nhưng gọn tay và ít gọi property lặp. if (controller != null) { ccRadius = controller.radius; ccSkin = controller.skinWidth; } } private void Start() { playerStateMachine.InitState(moveState); // btnJump.onClick.AddListener(HandleJump); } private void Update() { // Nếu có thay đổi runtime, có thể lấy lại mỗi vài giây/Start nếu bạn thích: // ccRadius = controller.radius; ccSkin = controller.skinWidth; playerStateMachine.UpdateState(); } public void HandleMovement() { // 1) Kiểm tra grounded bằng SphereCast ngắn dựa trên radius + skinWidth isGrounded = GroundCheck(out lastGroundHit); // 2) Input tạm thời: giữ nguyên như bạn if (UnityEngine.Input.GetKeyDown(KeyCode.LeftShift)) SetStatusRun(true); if (UnityEngine.Input.GetKeyUp(KeyCode.LeftShift)) SetStatusRun(false); if (UnityEngine.Input.GetKeyDown(KeyCode.Space)) HandleJump(); // 3) Trọng lực / sticky if (isGrounded && playerVelocity.y < 0f) { // Đè nhẹ để bám đất (tránh nhấp-nháy) playerVelocity.y = -2f; } else { playerVelocity.y += gravityValue * Time.deltaTime; } // 4) Chuyển động phẳng float x = joystick.Horizontal; float z = joystick.Vertical; Vector3 move = new Vector3(x, 0, z); move = Vector3.ClampMagnitude(move, 1f); if (move != Vector3.zero) { transform.forward = move; if (isRun) SetAnimRun(); else SetAnimWalk(); } else { SetAnimIdle(); } Vector3 finalMove = (move * playerSpeed) + (playerVelocity.y * Vector3.up); controller.Move(finalMove * Time.deltaTime); } private bool GroundCheck(out RaycastHit hit) { float radius = controller.radius; float skin = controller.skinWidth; float height = controller.height; // Tâm capsule theo world Vector3 cWorld = transform.TransformPoint(controller.center); float hemi = Mathf.Max(0f, (height * 0.5f) - radius); // Hai điểm top/bottom của capsule nhân vật (đang đứng) Vector3 pTop = cWorld + Vector3.up * hemi; Vector3 pBottom = cWorld - Vector3.up * hemi; // Ta tạo một "đoạn capsule ngắn" gần đáy để sweep xuống // Nhấc đoạn bắt đầu lên 1 chút để không bắt đầu trong trạng thái giao nhau Vector3 startBottom = pBottom + Vector3.up * (skin + 0.01f); Vector3 startTop = startBottom + Vector3.up * (radius * 2f - 0.02f); // chiều cao đoạn ngắn ~2*radius float castRadius = Mathf.Max(0f, radius - radiusEpsilon); float castDistance = skin + extraGroundDistance; // quãng sweep ngắn bool hitSomething = Physics.CapsuleCast( startTop, startBottom, castRadius, Vector3.down, out hit, castDistance, groundMask, QueryTriggerInteraction.Ignore ); if (!hitSomething) return false; // Lọc theo slope limit float maxSlope = controller.slopeLimit + slopeToleranceDeg; float slope = Vector3.Angle(hit.normal, Vector3.up); if (slope > maxSlope) return false; return true; } private void HandleJump() { if (isGrounded) { playerVelocity.y = Mathf.Sqrt(jumpHeight * -2f * gravityValue); SetAnimJump(); } } public void SetStatusRun(bool value) { if (!isGrounded) { Debug.LogError("Player not in ground"); return; } isRun = value; } public void InitCharacter(RoleInfo role) { string roleName = "(Error decoding name)"; if (role.name != null && role.name.ByteArray != null) { roleName = Encoding.UTF8.GetString(role.name.ByteArray, 0, role.name.Length); } Vector3 pos = new Vector3(role.posx, role.posy, role.posz); if (txtName != null) txtName.text = roleName; transform.position = pos; Debug.LogError("Pos Character = " + pos); } private void SetAnimIdle() { if (stateAnim == StateAnim.Idle || !isGrounded) return; stateAnim = StateAnim.Idle; animator.SetTrigger("Idle"); } private void SetAnimRun() { if (stateAnim == StateAnim.Run || !isGrounded) return; stateAnim = StateAnim.Run; animator.SetTrigger("Run"); } private void SetAnimWalk() { if (stateAnim == StateAnim.Walk || !isGrounded) return; stateAnim = StateAnim.Walk; animator.SetTrigger("Walk"); } private void SetAnimJump() { if (stateAnim == StateAnim.Jump) return; stateAnim = StateAnim.Jump; // Tạm dùng Idle trigger như code cũ của bạn animator.SetTrigger("Idle"); } } public enum StateAnim { Idle = 1, Walk = 2, Run = 3, Jump = 4 }