Phương pháp chung


Ở phần 3, ta đã thiết kế một lớp để quản lý vòng đời của game. Tuy nhiên, khó khăn dễ thấy là game thường rất lớn. Nếu tất cả mọi cập nhật game đều để trong hàm Run(), thì việc quản lý trở nên rất khó khăn. Để giải quyết vấn đề này, ta có thể áp dụng một phương pháp cũ nhưng hiệu quả: chia để trị.

Phương pháp chia để trị này được áp dụng trong tất cả các ứng dụng, thông qua việc áp dụng lượt đồ State diagram:


Áp dụng mô hình trên, game sẽ được chia thành nhiều state. Vấn đề nãy sinh là state trong game là gì, và chia như thế nào?

State và cách chia state trong game


State trong game, có thể hiểu là một giai đoạn của game, hay một màn hình/ 1 cảnh / 1 scene (tùy theo cách gọi, cách hiểu). 

Khi chơi một game, lấy vị dụ như game Angry Birds trên trình duyệt Chrome:


  • Logo nhà sản xuất
  • Màn hình giới thiệu game (poster)
  • Menu chính (gồm các nút play, option ...)
  • Các menu phụ chọn màn chơi
  • Loading trước khi vào màn chơi
  • Màn chơi
  • ....

Mỗi giai đoạn như vậy, ta có thể gọi là một state, hay một scene. ta chọn khái niệm state để gần gũi với mô hình UML.


State vs sub-State


Với một state quá lớn, ta lại nghĩ đến trường hợp chia nhỏ state thành các sub-state. Tuy nhiên, nên thận trọng trong việc chia sub-state. Việc chia thành các sub-state bên trong state đòi hỏi chi phí quản lý cao hơn. Do đó, không nên nếu không thật sự cần thiết.

Thiết kế State như thế nào ?


Nhìn chung, State cũng giống như một ứng dụng thu nhỏ, do đó cũng có các bước cơ bản sau:


  • Init
  • Run
  • Exit

Tuy nhiên, để tách biệt giữ xử lý logic và việc vẽ lên màn hình, Run nên được chia thành 2 bước nhỏ: Update (dành cho xử lý logic) và Render (dành cho việc vẽ). Việc chia tách này mang lại cho ta nhiều lợi ích. Bạn nào đã từng làm qua MVC, chắc hẳn biết được lợi ích của nó. Lúc này, các thao tác cần có của một State bao gồm:



  • Init
  • Update
  • Render
  • Exit






1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/// CState.h
#ifndef __CSTATE_H__
#define __CSTATE_H__
namespace GameTutor
{
    class CState
    {
    public:
        CState(){}
 virtual ~CState(){}
        virtual void Init() = 0;
        virtual void Update() = 0;
        virtual void Render() = 0;
        virtual void Exit() = 0;
    };
}
#endif



Quản lý các State như thế nào?



Sau khi đã chia nhỏ các state, nhiệm vụ tiếp theo là làm sao đảm bảo việc chuyển đổi giữa các state được trơn tru. 

Việc quản lý các state nhìn chung là do vòng lặp chính điều khiển. Tuy nhiên để thuận tiên, ta có thể định nghĩa 1 lớp chuyển quản lý các state, tạm gọi là lớp CStateManagement

Để đơn giản, việc quản lý state tuân theo nguyên tắc sau:


  • Tại một thời điểm, chỉ có 1 state được phép "hoạt động" (Update/Render)
  • Khi chuyển từ một state (A) sang một state khác (B), A phải được hủy (Exit) và B phải được tạo (Init) sau đó
  • Chỉ chuyển state (chuyển sang state khác) khi State cũ đã kết thúc việc update & render.



1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
/// CStateManagement.h
#ifndef __CSTATEMANAGEMENT_H__
#define __CSTATEMANAGEMENT_H__
#include "CState.h"
namespace GameTutor
{
    class CStateManagement
    {
    public:
        static CStateManagement* GetInstance()
        {
            if (!s_pIntance)
            {
                s_pIntance = new CStateManagement();
            }
            return s_pIntance;
        }
    protected:
        static CStateManagement* s_pIntance;
    protected:
        CStateManagement():m_pCurrentState(0), m_pNextState(0) {}
    protected:
        CState* m_pCurrentState;
        CState* m_pNextState;
    public:
        void Update(bool isPause);
        void SwitchState(CState* nextState);
    };
}
#endif




Trong file trên, CStateManagement được thiết kế theo kiểu Singleton pattern. Điều này đảm bảo lớp CStateManagement tồn tại duy nhất 1 instance trong suốt game.

Tiếp theo, ta xem ví dụ mẫu về cách quản lý state thông qua hàm update và switch state:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
/// CStateManagement.cpp
#include "CStateManagement.h"
namespace GameTutor
{
    CStateManagement* CStateManagement::s_pIntance = 0;
    void CStateManagement::Update(bool isPause)
    {
        // check if need switch state
        if (m_pCurrentState != m_pNextState)
        {
            if (m_pCurrentState)
            {
                m_pCurrentState->Exit();
                delete m_pCurrentState;
            }
            if (m_pNextState)
            {
                m_pNextState->Init();
            }
            m_pCurrentState = m_pNextState;
        }
        //update state
        if (m_pCurrentState)
        {
            if (!isPause)
            {
                m_pCurrentState->Update();
            }
            m_pCurrentState->Render();
        }
    }
    void CStateManagement::SwitchState(CState* nextState)
    {
        m_pNextState = nextState;
    }
}


Trong ví dụ trên, hàm Update mới là hàm quản lý chính việc chuyển state. SwitchState chỉ đóng vai trò "đánh dấu". Điều này đảm bảo CStateManagement hoạt động đúng theo 3 tiêu chí đã nêu ở trên.

Kết nối State và Game


Do việc quản lý State lúc này được trao cho CStateManagement. Ta chỉ việc kết nối CStateManagement và CGame.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
/// CGame.h
#ifndef __CGAME_H__
#define __CGAME_H__
namespace GameTutor
{
    class CGame
    {
    public:
        static CGame* GetInstance() {return s_pInstance;}
        virtual ~CGame() {}
        virtual void Run();
        virtual void Exit();
        virtual void Pause();
        virtual void Resume();
        bool IsAlive() {return m_isAlived;}
        bool IsPause() {return m_isPaused;}
    protected:
        CGame();
        static CGame* s_pInstance;
        virtual void Init() = 0;
        virtual void Destroy() = 0;
    protected:
        bool m_isAlived;
        bool m_isPaused;
    };
}
#endif



1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
/// CGame.cpp
#include "CGame.h"
#include "stdio.h"
#include "windows.h"
#include "CStateManagement.h"
namespace GameTutor
{
    CGame* CGame::s_pInstance = 0;
    CGame::CGame(): m_isPaused(false), m_isAlived(true)
    {
        s_pInstance = this;
    }
    void CGame:: Pause()
    {
        m_isPaused = true;
    }
    void CGame::Resume()
    {
        m_isPaused = false;
    }
    void CGame::Exit()
    {
        m_isAlived = false;
    }
    void CGame::Run()
    {
        this->Init();
        while (m_isAlived)
        {
            if (m_isPaused)
            {
                CStateManagement::GetInstance()->Update(true);
            }
            else
            {
                CStateManagement::GetInstance()->Update(false);
            }
            Sleep(80);
        }
        Destroy();
    }
}


Ví dụ sử dụng State và chuyển state


Giả sử ta có 2 State: 


  • State Logo: xuất ra màn hình từ 1 đến 10. Sau đó chuyển qua state Poster
  • State Poster: xuất ra màn hình từ 10 tới 1. Sau đó kết thúc game.



1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/// CStateLogo.h
#ifndef __CSTATELOGO_H__
#define __CSTATELOGO_H__
#include "CState.h"
using namespace GameTutor;
class CStateLogo: public CState
{
public:
    CStateLogo();
    ~CStateLogo() {}
    void Init();
    void Update();
    void Render();
    void Exit();
private:
    int m_iCount;
};
#endif



1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
/// CStateLogo.cpp
#include "CStateLogo.h"
#include "CStateManagement.h"
#include "CStatePoster.h"
#include
CStateLogo::CStateLogo():m_iCount(0), CState()
{}
void CStateLogo::Init()
{
    printf("State Logo: Init\n");
    m_iCount = 0;
}
void CStateLogo::Update()
{
    m_iCount++;
    if (m_iCount >= 10)
    {
        CStateManagement::GetInstance()->SwitchState(new CStatePoster());
    }
}
void CStateLogo::Render()
{
    printf("State Logo: %d\n", m_iCount);
}
void CStateLogo::Exit()
{
    printf("State Logo: Exit\n");
}


Trong đoạn code trên, hàm Update và Render minh họa việc tách biệt giữ Render và Update. CGameExample được hiệu chỉnh để khởi tạo state CStateLogo


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/// CExample.cpp
#include "CExample.h"
#include "CStateLogo.h"
#include "CStateManagement.h"
#include "stdio.h"
void CExample::Init()
{
    CStateManagement::GetInstance()->SwitchState(new CStateLogo());
    printf("Init\n");
}
void CExample:04estroy()
{
    printf("Destroy\n");
}


Kết luận



  • Để việc quản lý được đơn giản, ta cần chia chương trình (game) thành các state nhỏ.
  • Để thuận tiện, trước khi bắt đầu code game, ta nên vẽ trước state diagram, phác họa các state cần thiết, cũng như "đường đi" giữa các state.

Đến thời điểm này, thư viện ta đã xây dựng được 3 lớp cơ bản:



  • CGame
  • CState
  • CStateManagement



Post a Comment Blogger

 
Top