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”

statemachine

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.

app-store-update-december-9-20111209041100340

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.

statemachine_demo

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_mess

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.

stunn.jpg

Ý 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ó.

9 thoughts on “CoReaction – một hướng tư duy khác, thay thế cho StateMachine – P1

    1. à, 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

      Like

Leave a comment