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:
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:estroy() { 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 Facebook
Post a Comment