Bài hôm nay sẽ hơi lạ một chút, nói về CoReactions – từ này nghe có vẻ mới mới. Trải qua kha khá các project về game, trong quá trình design codebase, chắc nhiều người cũng như mình, sử dụng StateMachine khá nhiều. Cá nhân mình thấy StateMachine hơi khó dùng, bị đơn điệu và rất khó để mở rộng. Sau đó, tổng hợp từ vài bài viết + cãi lộn tham khảo các đại ka, đồng nghiệm khác, mình phác thảo ra một giải pháp hay hơn, “hơi” tổng hợp từ một vài pattern, và tự đặt tên nó là CoroutineReactions =))) , cho nên mấy bạn đừng có Google, cũng ko có đâu =)).
Phần 1 mình sẽ trình bày sơ lược về StateMachine và những điểm yếu, qua phần 2 sẽ nói về tư duy Coreaction, và phần 3 sẽ làm một demo nhỏ.
Ôn bài chút với StateMachine “huyền thoại”
Chả lạ lẫm gì ba cái đồ StateMachine nữa. Cái này dễ gặp khi làm các game Platformer, điều khiển nhân vật. Và các state thường thấy sẽ là Idle, Run, Jump, Dead ..v.v.. Bắt đầu từ một đề bài thực tế nhé : mình cần làm một game Platformer2D đơn giản, nhân vật chính, tạm thời chỉ có 3 state cơ bản : Walk, Jump, Dead.
Và mình sẽ có một đoạn mô tả ngắn ngắn về 3 state thế này :
- Walk : trạng thái cơ bản, nhân vật đứng trên platform, có thể dùng phím di chuyển qua lại, hoặc nhảy. Từ Walk state sẽ chuyển qua 2 trạng thái kia theo mô tả :
– Health = 0 : chuyển qua Dead state.
– OnGround = FALSE : chuyển qua Jump state (khi nhân vật bấm nhảy) - Jump : nhân vật đã ở trên không, không được phép bấm nhảy lần nữa (ko nói mấy game có double jump nhé, đề bài đơn giản mà :v ). Cách chuyển qua 2 state kia :
– Health = 0 : chuyển qua Dead state
– OnGround = TRUE : chuyển về Walk state (khi nhân vật chạm đất) - Dead : người chơi không thể điều khiển nhân vật được nữa.
Zòi, nói về code thì cũng chẳng có gì phức tạp lắm. Nếu đơn giản thì chỉ cần làm trong 1 class, Update rồi if..else này kia chút, còn “ngầu” hơn thì có thể viết riêng mỗi state một class, kế thừa từ BaseState gì gì đó, rồi thêm class StateController … Nhưng gì thì gì, trong mỗi State, mình cũng sẽ xoay quanh các thao tác sau :
- Nhận và xử lý các input, event trong mỗi state
vd: Walk state, chỉ cho phép người chơi bấm A,D để qua lại, Space để nhảy
vd: Jump state, người chơi ko đc phép bấm Space để nhẩy lần nữa, chỉ được bấm A,D để di chuyển qua lại - Kiểm tra điều kiện để chuyển qua state khác
vd: Walk state, khi bấm Space, add lực làm nhân vật nhảy lên, khi đó OnGround = false, thì chuyển qua Jump state.
Từ Jump state, trong Update, kiểm tra OnGround = true để chuyển về Walk state
Code ngắn ngắn thì sẽ trông như thế này :
Public MainPlayerController : MonoBehavior | |
{ | |
CharacterState _currentState; | |
void Start() | |
{ | |
_currentState = CharacterState.Walk; | |
} | |
void Update() | |
{ | |
switch (_currentState) | |
{ | |
case CharacterState.WALK: | |
UpdateWalkState(); | |
case CharacterState.JUMP: | |
UpdateJumpState(); | |
case CharacterState.DEAD: | |
UpdateDeadState(); | |
} | |
} | |
void UpdateWalkState() | |
{ | |
// checking input to move character | |
if (Input.GetKey(Keycode.A)) {//...} | |
if (Input.GetKey(Keycode.D)) {//...} | |
// change state | |
if (Input.GetKeyDown(Keycode.Space)) | |
{ | |
// add force to rigidbody | |
rigidbody.AddForce(...); | |
// change state | |
_currentState = CharacterState.JUMP; | |
} | |
} | |
void UpdateJumpState() | |
{ | |
// checking onGround | |
if (IsOnGround()) _currentState = CharacterState.WALK; | |
} | |
void UpdateDeadState() | |
{ | |
} | |
bool IsOnGround() | |
{ | |
// Use raycast, line cast ... to know | |
// if character are on Platforms or not | |
} | |
} |
……. Và rồi rắc rối bắt đầu xảy ra …
Cái đề bài trên chỉ là rất ngắn và đơn giản, mình chỉ giới thiệu sơ lại StateMachine, một cách tổng quát, chứ trong thực tế, chả có cái game nào mà StateMachine chỉ có 3 state và các điều kiện đơn giản như vậy đâu. Như một project platformer mình từng làm qua, nhân vật chính lúc đầu cũng có vài state cơ bản : Idle (đứng yên), Jump (trên không), Dead. Rồi vài bữa, nhân vật có thể cầm thêm vũ khí, thế là sinh thêm ra state : IdleNormal (đứng yên không cầm vũ khí), IdleCarrying (đứng yên có cầm vũ khí), JumpCarrying (nhảy trên không có cầm vũ khí) … rồi vài bữa ..v.v… Và nếu như xài StateMachine, có thể bạn sẽ kết thúc với cái đống state trông như thế này :
StateMachine hạn chế chỗ nào nhỉ ?
Đơn “luồng”
Cái này dễ hiểu, trong cùng một lúc, chỉ có 1 state duy nhất được chạy, bạn phải làm cả 2 việc như mình đã liệt kê ở trên : nhận xử lý input + kiểm tra chuyển state. Điều này sẽ khiến code của bạn “tòe loe” lên khi mà input bắt đầu nhiều lên (input ở đây là bao gồm cả user input – keyboard, mouse.. + những event này nọ nữa nhé), và phần code logic để chuyển state sẽ “khủng” nếu như số lượng state nhiều.
Khó mở rộng và bảo trì
Vì sẽ rất khó khăn khi muốn thêm một state mới, một input mới, vì hầu như các state bị “coupling” với nhau khá nhiều. Mỗi lần thêm một state mới, bạn sẽ phải sửa cả những state cũ, thêm các điều kiện luân chuyển state. Tin mình đi, code của bạn, sau một hồi sẽ là một đống bùi nhùi gồm if..else tùm lum tà la.
Ý tưởng về một giải pháp thay thế – CoReactions
Nhìn vấn đề ở mức tổng quát hơn
Mục đích cuối cùng của việc phân tách thành các state là bởi vì phản ứng của nhân vật đối với từng input (user input + game events) trong từng state là khác nhau. Và cái đống if..else trong từng StateUpdate là thực hiện các việc :
Trong State X,
+ nếu có input Y1, thì nhân vật sẽ phản ứng Z1
+ nếu có input Y2, thì nhân vật sẽ phản ứng Z2 ..v.v..
Vd: Walk state, nhận input là user bấm Space, thì nhân vật nhảy lên. Nhưng trong JumpState, khi nhận input là user bấm Space, thì nhân vật không phản ứng lại. Hoặc như trong DeadState, thì input có là gì nhân vật cũng không phản ứng lại.
Phân tách thành các thành phần chức năng
Như vậy, những thành phần tạo nên logic cho một state sẽ là :
- Input : (nhắc lại) user input và game events
- Reaction : những action sẽ được thực thi khi nhận được input
VD: Với WalkState
– input : user bấm phím A,D -> reaction : add force lên nhân vật để di chuyển trái phải
– input : user bấm Space -> reaction : AddForce để nhân vật nhảy + chuyển state
Với JumpState
– input : event OnGround=True -> reaction : chuyển state về Walk
..v.v….
Vậy ý tưởng là : có cách nào không dùng state, quy tất cả về một danh sách các Reaction, các reaction sẽ được thực thi khi đảm bảo có đủ các điều kiện (condition) nào đó (user input, game events …). Và khi có thêm input, event … chỉ việc thêm Reaction mới mà không cần chỉnh sửa những Reaction cũ ?
Gom góp
Thoy, phần 1 thì mình giới thiệu cơ bản thế, tránh bài viết quá dài, qua phần 2 mình sẽ trình bày về CoReaction, các thành phần bên trong nó.
Đang định hỏi dạo này ko thấy a ra bài mới nữa :V
LikeLiked by 1 person
Ngầu quá a ơi, lâu ghê mới thấy a viết bài mới
LikeLiked by 1 person
ý tưởng hay, cơ mà với animator thì mình thấy thêm cái blend tree nữa là đủ @@
LikeLike
à, Animator component của Unity thì ổn rồi, chỉ là mình học cách nó design hệ thống Parameter, Conditions của nó vào trong design codebase. Như mình có gói gọn, kiểu design này rút ra từ thực tế áp dụng, có thể còn thiếu sót hoặc cải tiến ;). Cám ơn bạn có đóng góp ý kiến
LikeLike
Lâu lâu mới thấy bạn viết bài ^ – ^. Lại biết thêm khái niệm khá hay. Thanks you!
LikeLiked by 1 person
CoReaction làm mình cứ tưởng là Reaction Controller chứ ^^
LikeLiked by 1 person
Hehe, thì cũng giống như ObserverPattern nhưng đâu có class Observer đâu :v
LikeLike