Bài tập: chơi được bằng một ngón tay
Assets/
Scenes/
Arena.unity
Scripts/
Settings/
everything from tab 01–03
đã có sẵnbài này thêm
Cho Arena một bộ điều khiển chạy được trên bàn phím, tay cầm và cảm ứng chỉ từ một action map — kèm camera góc nhìn thứ ba không xuyên tường, không giật, rung khi trúng đòn, và chuyển sang góc quay đẹp khi dùng chiêu cuối.
dùng lại những gì
Ba thiết bị, một action map, một bộ camera.
Phần 1 — một action map cho tất cả
Mỗi action phải có binding cho cả ba thiết bị. Nếu thấy mình đang viết if (touch) … else if (keyboard) … thì dừng lại — action map sinh ra chính là để tránh chuyện đó.
Map: Gameplay
Move Value / Vector2
├─ 2D Vector Composite W A S D
├─ Gamepad <Gamepad>/leftStick
└─ On-Screen Stick (gán từ prefab UI)
Look Value / Vector2
├─ Pointer delta <Mouse>/delta
├─ Gamepad <Gamepad>/rightStick
└─ Touch drag (nửa phải màn hình)
Jump Button
├─ <Keyboard>/space ├─ <Gamepad>/buttonSouth └─ On-Screen Button
Skill1 / Skill2 / Skill3 Button
├─ Q E R ├─ West / North / buttonEast └─ On-Screen Buttons
Pause Button
├─ <Keyboard>/escape └─ <Gamepad>/start
Map: UI ← chỉ bật khi tạm dừng
Navigate · Submit · Cancelusing UnityEngine;
using UnityEngine.InputSystem;
public class InputRouter : MonoBehaviour
{
private PlayerControls controls;
public Vector2 Move { get; private set; }
public Vector2 Look { get; private set; }
public bool JumpQueued { get; private set; }
[SerializeField] private SkillCaster caster;
void Awake()
{
controls = new PlayerControls();
var g = controls.Gameplay;
g.Move.performed += c => Move = c.ReadValue<Vector2>();
g.Move.canceled += _ => Move = Vector2.zero;
g.Look.performed += c => Look = c.ReadValue<Vector2>();
g.Look.canceled += _ => Look = Vector2.zero;
g.Jump.performed += _ => JumpQueued = true;
g.Skill1.performed += _ => caster.TryCast(0);
g.Skill2.performed += _ => caster.TryCast(1);
g.Skill3.performed += _ => caster.TryCast(2);
g.Pause.performed += _ => TogglePause();
}
void OnEnable() => controls.Enable();
void OnDisable() => controls.Disable();
void LateUpdate() => JumpQueued = false;
void TogglePause()
{
bool paused = Time.timeScale > 0f;
Time.timeScale = paused ? 0f : 1f;
//đổi map đang bật
if (paused) { controls.Gameplay.Disable(); controls.UI.Enable(); }
else { controls.UI.Disable(); controls.Gameplay.Enable(); }
Cursor.lockState = paused ? CursorLockMode.None : CursorLockMode.Locked;
Cursor.visible = paused;
}
}Phần 2 — bộ camera
Một camera Cinemachine kiểu Third Person Follow, Follow = Knight, LookAt = CameraTarget đặt ngang ngực.
Camera Collision Filter chọn layer Environment, để nó tự thu vào khi gặp tường thay vì xuyên qua.
Damping 0.3 cho cả ba trục — đủ nặng tay, chưa tới mức lảo đảo.
Camera thứ hai cho chiêu cuối, bình thường Priority 0, khi bấm R thì nâng lên 20 trong 1,5 giây.
Impulse Listener trên cả hai camera, Impulse Source trên nhân vật, kích hoạt mỗi lần trúng đòn.
using System.Collections;
using UnityEngine;
using Unity.Cinemachine;
public class CameraDirector : MonoBehaviour
{
[SerializeField] private CinemachineCamera gameplayCam;
[SerializeField] private CinemachineCamera ultimateCam;
[SerializeField] private CinemachineImpulseSource impulse;
//gọi từ SkillCaster khi trúng đòn
public void Shake(float strength = 0.35f) => impulse.GenerateImpulseWithForce(strength);
//gọi khi bắt đầu chiêu cuối
public void PlayUltimateShot(float seconds = 1.5f) => StartCoroutine(Cut(seconds));
IEnumerator Cut(float seconds)
{
ultimateCam.Priority = 20;
yield return new WaitForSeconds(seconds);
ultimateCam.Priority = 0;
}
}Phần 3 — lớp cảm ứng
Canvas đặt Scale With Screen Size, tham chiếu 1080 × 1920, Match 0.5.
Góc dưới trái: On-Screen Stick gán vào Move. Góc dưới phải: ba nút skill và nút nhảy.
SafeAreaFitter trên panel gốc, và mọi nút neo vào góc, đừng neo giữa.
Cả canvas cảm ứng bị tắt trên máy tính — kiểm tra Application.isMobilePlatform trong Awake.
void Awake()
{
bool mobile = Application.isMobilePlatform;
#if UNITY_EDITOR
mobile = forceTouchInEditor; // tick để thử trên PC
#endif
touchCanvas.SetActive(mobile);
}Khi nào thì xong
- ✓Cùng một bản build nhận cả bàn phím, tay cầm cắm vào, và nút cảm ứng — mà không có nhánh if nào trong code.
- ✓Bấm W luôn là đi ra xa camera, ở mọi góc quay.
- ✓Đi sát tường thì camera thu vào mượt mà thay vì để lộ phần bên trong bức tường.
- ✓Nhân vật không rung khi di chuyển — code camera nằm trong LateUpdate.
- ✓Mỗi cú trúng đòn camera rung nhẹ; rung hết trong vòng 0,25 giây.
- ✓Chiêu cuối chuyển sang camera thứ hai rồi tự chuyển mượt về.
- ✓Bấm Esc là tạm dừng, thả con trỏ, và input gameplay tắt — nhân vật không đi tiếp nữa.
- ✓Trong dự án không còn dòng Input.GetAxis hay Input.GetKey nào.
Con số cần đạt
| Chỉ số | Mục tiêu | Vì sao |
|---|---|---|
| Độ trễ camera | 0.2–0.4 s | Dưới 0,2 là dính cứng, trên 0,5 là người chơi lạc mất nhân vật. |
| Thời gian rung | ≤ 0.25 s | Lâu hơn là tưởng lỗi, và trên mobile gây chóng mặt. |
| Giới hạn góc ngẩng / cúi | -25° … 65° | Ra ngoài khoảng đó camera lộn qua đầu. |
| Kích thước vùng chạm | ≥ 44 pt | Nhỏ hơn là ngón cái bấm trượt liên tục trên máy thật. |
Muốn làm thêm
Thêm thanh trượt độ nhạy và nút đảo trục Y, giữ lại giữa các lần chơi.
Nhận diện thiết bị đang dùng và đổi gợi ý phím trên màn hình giữa Q/E/R và ký hiệu tay cầm.
Thêm camera khoá mục tiêu: bấm chuột giữa để ngắm vào kẻ địch gần nhất. (Phím Tab đã dùng để đổi nhân vật.)
Thu camera lại khi nhân vật đứng sát tường, dùng extension của Cinemachine.