Advertisement
  1. Game Development
  2. Game Engine Development

Cách triển khai và sử dụng Message Queue trong game của bạn

Scroll to top
Read Time: 20 min

Vietnamese (Tiếng Việt) translation by Andrea Ho (you can also view the original English article)

Game thường do một số thực thể khác biệt tạo thành, chúng tương tác với nhau. Những tương tác này rất sôi nổi và có kết nối rõ rệt đến gameplay (cách chơi). Hướng dẫn này nói đến các khai niệm và sự triển khai của hệ thống message queue, để đồng hoá các tương tác của thực thể, giúp code của bạn có thể quản lý và dễ bảo trì khi nó tăng trưởng phức tạp.

Giới thiệu

Một quả bom có thể tương tác với một nhân vật bằng cách gây nổ hoặc gây thương tổn, hộp y tế có thể cứu chữa một thực thể, chìa khoá dùng mở cánh cửa, và cứ vậy. Tương tác trong game là bất tận, nhưng làm sao ta có thể duy trì khả năng quản lý code trong khi vẫn có thể xử lý những tương tác đó? Làm sao ta chắc được code có thể thay đổi và vẫn hoạt động khi có nhiều tương tác mới và không mong đợi nảy sinh?

Interactions in a game tend to grow in complexity very quicklyInteractions in a game tend to grow in complexity very quicklyInteractions in a game tend to grow in complexity very quickly
Tương tác trong game có xu hướng tăng trưởng phức tạp nhanh chóng.

Khi tương tác được bổ sung (đặc biệt là những tương tác không mong muốn), thì code của bạn là càng lúc càng lộn xộn. Một triển khai ngờ nghệch sẽ nhanh chóng khiến bạn đặt ra những câu hỏi như:

"Đây là thực thể A, vậy tôi nên gọi phương thức damage() cho nó, đúng không?" Hoặc nó là damageByItem() phải không? Có lẽ là damageByWeapon() mới đúng?

Hình dung xem mớ hỗn độn dàn trải khắp tất cả thực thể game của bạn, bởi vì tất cả đều tương tác với nhau theo cách riêng biệt. Thật may, có một phương pháp quản lý tốt hơn, đơn giản hơn để thực hiện việc này.

Message Queue

Xem xét message queue. Ý tưởng cơ bản đăng sau khái niệm này là để triển khai tất cả tương tác game như một hệ thống giao tiếp (vẫn được dùng đến hôm nay): message exchange (trao đổi thông điệp). Mọi người đã giao tiếp thông qua message (các ký tự) nhiều thế kỷ vì đó là một hệ thống hữu hiệu và đơn giản.

Trong dịch vụ bưu điện thực tế, nội dung mỗi message có thể khác biệt, nhưng cách gửi và nhận chúng đều như nhau. Người gửi đưa thông tin vào trong bao thư và đưa nó đến địa chỉ đích đến. Nơi đến có thể có (hoặc không) phản hồi theo cùng một cơ chế, chỉ bằng việc thay đổi mục "from/to" trên bao thư.

Interactions made using a message queue systemInteractions made using a message queue systemInteractions made using a message queue system
Tương tác được xây dựng nhờ vào hệ thống message queue.

Áp dụng khái niệm này vào game, tất cả tương tác giữa các thực thể có thể xem như là message. Nếu thực thể game muốn tương tác với thực thể khác (hoặc một nhóm thực thể), thì tất cả thực thể đều phải gửi đi một message. Điểm tiếp nhận sẽ xử lý hoặc phản ứng với message dựa trên nội dung của nó và người gửi là ai.

Trong phương pháp này, giao tiếp giữa các thực thể game trở nên thống nhất. Tất cả thực thể có thể gửi và nhận message. Dù tương tác hoặc message có phức tạp thế nào, kênh giao tiếp vẫn hoạt động như nhau.

Xuyên suốt phần tiếp theo, tôi sẽ mô tả cách bạn có thể thực sự triển khai phương pháp message queue trong game của mình.

Thiết kế một Envelope (Message)

Hãy bắt đầu bằng cách thiết kế một envelope (phong bì), là yếu tố cơ bản nhất trong hệ thống message queue.

Envelope có thể được miêu tả như hình bên dưới:

Structure of a messageStructure of a messageStructure of a message
Cấu trúc của một message.

Hai field đầu tiên (senderdestination) là tham chiếu đến thực thể tạo ra và thực thể sẽ nhận message này, theo thứ tự. Sử dung những field này, cả sender (người gửi) và receiver (người nhận) có thể xác định message đi đến đâu và từ đâu đến.

Hai field khác (typedata) hoạt động cùng nhau để bảo đảm message được xử lý đúng. Field type miêu tả message này là gì, ví dụ nếu type là "damage", receiver sẽ xử lý message như một mệnh lện để giảm điểm sức khoẻ xuống; nếu type là "purse", receiver sẽ xem đó là một hướng dẫn để theo đuổi điều gì đó - và cứ thế.

Field data trực tiếp liên kết đến field type. Trong ví dụ trước đó, nếu type là "damage", thì field data sẽ gồm một con số - 10, mô tả lượng tổn thương sẽ áp dụng cho điểm sửc khoẻ của receiver. Nếu type là "purse", thì "data" sẽ gồm một đối tượng mô tả mục tiêu phải được đuổi theo.

Trường data có thể chứa thông tin bất kỳ, giúp envelope có ý nghĩa giao tiếp đa dạng. Bất kỳ điều gì có thể đặt vào field này, integer, float, string, và thậm chí object. Nguyên tắc quan trọng đầu tiên là receiver phải biết field data có gì dựa trên field type là gì.

Tất cả lý thuyết này có thể chuyển thành class đơn giản Message. Class này có 5 properties (thuộc tính), mỗi property cho từng field.

1
Message = function (to, from, type, data) {
2
    // Properties

3
    this.to = to;        // a reference to the entity that will receive this message

4
    this.from = from;    // a reference to the entity that sent this message

5
    this.type = type;    // the type of this message

6
    this.data = data;    // the content/data of this message

7
};

Ví dụ cho cách dùng là nếu thực thể A muốn gửi một message "damage" đến thực thể B, tất cả việc nó phải làm là khởi tạo một object của class Message, xét property to thành B, xét property from thành A, xét type thành "damage", và sau cùng xét "data" thành một con số (ví dụ như 10):

1
// Instantiate the two entities

2
var entityA = new Entity();
3
var entityB = new Entity();
4
5
// Create a message to entityB, from entityA,

6
// with type "damage" and data/value 10.

7
var msg = new Message();
8
9
msg.to = entityB;
10
msg.from = entityA;
11
msg.type = "damage";
12
msg.data = 10;
13
14
// You can also instantiate the message directly

15
// passing the information it requires, like this:

16
var msg = new Message(entityB, entityA, "damage", 10);

Giờ ta có cách để tạo ra message, giờ là lúc nghĩ đến class sẽ lưu và truyền tải nó.

Triển khai một queue

Class chịu trách nhiệm lưu và truyền tải các message sẽ gọi là MessageQueue. Nó hoạt động như một bưu điện, tất cả message được đưa về class này, nó bảo đảm các message được chuyển đến các điểm đích.

Hiện giờ, class MessageQueue sẽ có cấu trúc rất đơn giản:

1
/**

2
 * This class is responsible for receiving messages and

3
 * dispatching them to the destination.

4
 */
5
MessageQueue = function () {
6
    this.messages = [];  // list of messages to be dispatched

7
};
8
9
// Add a new message to the queue. The message must be an

10
// instance of the class Message.

11
MessageQueue.prototype.add = function(message) {
12
    this.messages.push(message);
13
};

Thuộc tính messages là array (mảng). Nó sẽ lưu tất cả message chuẩn bị được truyền đi bằng MessageQueue. Phương thức add() nhận object của class Message làm đối số, và bổ sung object đó vào danh sách của message.

Đây là cách thực A thông báo cho thực thể B về thương tổn sẽ được dùng trong class MessageQueue:

1
// Instantiate the two entities and the message queue

2
var entityA = new Entity();
3
var entityB = new Entity();
4
var messageQueue = new MessageQueue();
5
6
// Create a message to entityB, from entityA,

7
// with type "damage" and data/value 10.

8
var msg = new Message(entityB, entityA, "damage", 10);
9
10
// Add the message to the queue

11
messageQueue.add(msg);

Giờ chúng ta có một cách để tạo ra và lưu message trong queue (hàng). Giờ là lúc truyền tải chúng đến các điểm nhận.

Truyền tải message

Để làm cho class MessageQueue thực sự gửi message, đầu tiên chúng ta cần xác định cách mà các thực thể sẽ xử lý và nhận message. Phương pháp dễ nhất là bổ sung vào môt phương thức onMessage() giúp mỗi thực thể có thể nhận được message:

1
/**

2
 * This class describes a generic entity.

3
 */
4
Entity = function () {
5
    // Initialize anything here, e.g. Phaser stuff

6
};
7
8
// This method is invoked by the MessageQueue

9
// when there is a message to this entity.

10
Entity.prototype.onMessage = function(message) {
11
    // Handle new message here

12
};

Class MessageQueue sẽ gọi phương thức onMessage() của mỗi thực thể nhận message. Tham số truyền vào phương thức này là message được hệ thống message truyền đi (và được nhận ở điểm đến).

Class MessageQueue sẽ hướng message trong hàng của nó một lần, trong phương thức dispatch():

1
/**

2
 * This class is responsible for receiving messages and

3
 * dispatching them to the destination.

4
 */
5
MessageQueue = function () {
6
    this.messages = [];  // list of messages to be dispatched

7
};
8
9
MessageQueue.prototype.add = function(message) {
10
    this.messages.push(message);
11
};
12
13
// Dispatch all messages in the queue to their destination.

14
MessageQueue.prototype.dispatch = function() {
15
    var i, entity, msg;
16
17
    // Iterave over the list of messages

18
    for(i = 0; this.messages.length; i++) {
19
        // Get the message of the current iteration

20
        msg = this.messages[i];
21
22
        // Is it valid?

23
        if(msg) {
24
            // Fetch the entity that should receive this message 

25
            // (the one in the 'to' field)

26
            entity = msg.to;
27
28
            // If that entity exists, deliver the message.

29
            if(entity) {
30
                entity.onMessage(msg);
31
            }
32
33
            // Delete the message from the queue

34
            this.messages.splice(i, 1);
35
            i--;
36
        }
37
    }
38
};

Phương thức này chạy qua tất cả message trong queue và mỗi message, field to được dùng để lấy một tham chiếu dến receiver. Phương thức onMessage() của receiver sau đó được gọi, với message hiện tại làm tham số, và message đã truyền đi sau đó được xoá ra khỏi danh sách MessageQueue. Quá trình này lặp lại cho đến khi tất cả message được truyền đi.

Sử dụng message queue

Đây là lúc nhìn lại tất cả chi tiết của triển khai này hoạt động cùng nhau. Hãy dùng hệ thống message queue trong một demo rất đơn giản gồm một vài thực thể đang di chuyển và tương tác với nhau. Để cho đơn giản, chúng ta sẽ có 3 thực thể: Healer, RunnerHunter.

Runner có thanh sức khoẻ và di chuyển ngẫu nhiên xung quanh. Healer sẽ chữa trị cho Runner bất kỳ chạy ngang qua, mặt khác, Hunter sẽ gây thiệt hại cho bất kỳ Runner xung quanh. Tất cả tương tác sẽ được xử lý bằng hệ thống message queue.

Bổ sung Message Queue

Hãy bắt đầu tạo ra PlayState sẽ gồm có một danh sách các thực thể (healer, runner và hunter) và một giá trị của class MessageQueue:

1
var PlayState = function() {
2
    var entities;  	// list of entities in the game

3
	var messageQueue;	// the message queue (dispatcher)

4
5
	this.create = function() {
6
		// Initialize the message queue

7
		messageQueue = new MessageQueue();
8
9
		// Create a group of entities.

10
		entities = this.game.add.group();
11
	};
12
13
	this.update = function() {
14
		// Make all messages in the message queue

15
    	// reach their destination.

16
		messageQueue.dispatch();
17
	};
18
};

Trong vòng lặp game, được hiển thị bằng phương thức update(), phương thức dispatch() của message queue được kích hoạt, vậy tất cả message được truyền đi ở frame cuối cùng của mỗi game.

Bổ sung các Runner

Class Runner có cấu trúc như sau:

1
/**

2
 * This class describes an entity that just

3
 * wanders around.

4
 */
5
Runner = function () {
6
    // initialize Phaser stuff here...

7
};
8
9
// Invoked by the game on each frame

10
Runner.prototype.update = function() {
11
    // Make things move here...

12
}
13
14
// This method is invoked by the message queue

15
// to make the runner deal with incoming messages.

16
Runner.prototype.onMessage = function(message) {
17
    var amount;
18
19
    // Check the message type so it's possible to

20
    // decide if this message should be ignored or not.

21
    if(message.type == "damage") {
22
        // The message is about damage.

23
        // We must decrease our health points. The amount of

24
        // this decrease was informed by the message sender

25
        // in the 'data' field.

26
        amount = message.data;
27
        this.addHealth(-amount);
28
29
    } else if (message.type == "heal") {
30
        // The message is about healing.

31
        // We must increase our health points. Again the amount of

32
        // health points to increase was informed by the message sender

33
        // in the 'data' field.

34
        amount = message.data;
35
        this.addHealth(amount);
36
37
    } else {
38
        // Here we deal with messages we are not able to process.

39
        // Probably just ignore them :)

40
    }
41
};

Phần quan trọng nhất của phương thức onMessage(), được kích hoạt bởi message queue mỗi khi có một message cho giá trị này. Như đã giải thích trước đó, field type trong message được sử dụng để quyết định xem giao tiếp này là gì.

Dựa trên type của message, hành động đúng được thực hiện: nếu type là "damage", điểm sức khoẻ giảm, nếu type là "heal", điểm sức khoẻ tăng lên. Số điểm sức khoẻ tăng hay giảm được xác định bởi field data từ sender của message.

Trong PlayState, chúng ta thêm vào danh sách thực thể một vài runner:

1
var PlayState = function() {
2
    // (...)

3
4
	this.create = function() {
5
		// (...)

6
7
		// Add runners

8
		for(i = 0; i < 4; i++) {
9
			entities.add(new Runner(this.game, this.game.world.width * Math.random(), this.game.world.height * Math.random()));
10
		}
11
	};
12
13
	// (...)

14
};

Kết quả là có thêm nhiều runner di chuyển ngẫu nhiên xung quanh:

Bổ sung Hunter

Class Hunter có cấu trúc như sau:

1
/**

2
 * This class describes an entity that just

3
 * wanders around hurting the runners that pass by.

4
 */
5
Hunter = function (game, x, y) {
6
    // initialize Phaser stuff here

7
};
8
9
// Check if the entity is valid, is a runner, and is within the attack range.

10
Hunter.prototype.canEntityBeAttacked = function(entity) {
11
    return  entity && entity != this &&
12
            (entity instanceof Runner) &&
13
            !(entity instanceof Hunter) &&
14
            entity.position.distance(this.position) <= 150;
15
};
16
17
// Invoked by the game during the game loop.

18
Hunter.prototype.update = function() {
19
    var entities, i, size, entity, msg;
20
21
    // Get a list of entities

22
    entities = this.getPlayState().getEntities();
23
24
    for(i = 0, size = entities.length; i < size; i++) {
25
        entity = entities.getChildAt(i);
26
27
        // Is this entity a runner and is it close?

28
        if(this.canEntityBeAttacked(entity)) {
29
            // Yeah, so it's time to cause some damage!

30
            msg = new Message(entity, this, "damage", 2);
31
32
            // Send the message away!

33
            this.getMessageQueue().add(msg); // or just entity.onMessage(msg); if you want to bypass the message queue for some reasong.

34
        }
35
    }
36
};
37
38
// Get a reference to the game's PlayState

39
Hunter.prototype.getPlayState = function() {
40
    return this.game.state.states[this.game.state.current];
41
};
42
43
// Get a reference to the game's message queue.

44
Hunter.prototype.getMessageQueue = function() {
45
    return this.getPlayState().getMessageQueue();
46
};

Hunter cũng sẽ di chuyển xung quanh, nhưng họ sẽ gây tổn thương cho các runner gần đó. Hành vi này được triển khai trong phương thức update(), ở đó tất cả thực thể trong game được kiểm tra và runner được thông tin về mức tổn hại.

Message cho thương tổn được tạo ra như sau:

1
msg = new Message(entity, this, "damage", 2);

Message chứa thông tin về nơi nhận (entity, trong trường hợp này là entity được phân tích trong vòng lặp hiện tại), sender (this, đại diện cho hunter đang tấn công) type của message ("damage") và số lượng tổn thương (2, trong trường hợp này được gán cho field data của message).

Message sau đó được chuyên đến điểm đến thông qua lệnh this.getMessageQueue().add(msg), lệnh này bổ sung message vừa được tạo ra vào message queue.

Sau cùng, ta bổ sung Hunter vào danh sách của thực thể trong PlayState:

1
var PlayState = function() {
2
    // (...)

3
4
    this.create = function() {
5
		// (...)

6
        
7
		// Add hunter at position (20, 30)

8
    	entities.add(new Hunter(this.game, 20, 30));
9
	};
10
11
	// (...)

12
};

Kết quả là có vài runner di chuyển xung quanh, nhận message từ hunter khi chúng tiến lại gần nhau:

Tôi đã bổ sung flying envelope làm hướng dẫn trực quan để giúp hiển thị điều đang diễn ra.

Bổ sung Healder

Class Healer có cấu trúc như sau:

1
/**

2
 * This class describes an entity that is

3
 * able to heal any runner that passes nearby.

4
 */
5
Healer = function (game, x, y) {
6
    // Initializer Phaser stuff here

7
};
8
9
Healer.prototype.update = function() {
10
    var entities, i, size, entity, msg;
11
12
    // The the list of entities in the game

13
    entities = this.getPlayState().getEntities();
14
15
    for(i = 0, size = entities.length; i < size; i++) {
16
        entity = entities.getChildAt(i);
17
18
        // Is it a valid entity?

19
        if(entity) {
20
            // Check if the entity is within the healing radius

21
            if(this.isEntityWithinReach(entity)) {
22
                // The entity can be healed!

23
                // First of all, create a new message regaring the healing

24
                msg = new Message(entity, this, "heal", 2);
25
26
                // Send the message away!

27
                this.getMessageQueue().add(msg); // or just entity.onMessage(msg); if you want to bypass the message queue for some reasong.

28
            }
29
        }
30
    }
31
};
32
33
// Check if the entity is neither a healer nor a hunter and is within the healing radius.

34
Healer.prototype.isEntityWithinReach = function(entity) {
35
    return !(entity instanceof Healer) && !(entity instanceof Hunter) && entity.position.distance(this.position) <= 200;
36
};
37
38
// Get a reference to the game's PlayState

39
Healer.prototype.getPlayState = function() {
40
    return this.game.state.states[this.game.state.current];
41
};
42
43
// Get a reference to the game's message queue.

44
Healer.prototype.getMessageQueue = function() {
45
    return this.getPlayState().getMessageQueue();
46
};

Code và cấu trúc tương tự với class Hunter, ngoại trừ vài chỗ khác biệt. Tương tự như triển khai của Hunter, phương thức update() của healer lặp lại trên danh sách của các thực thể trong game, truyền tin đến bất kỳ thực thể nào trong phạm vi cứu chữa:

1
msg = new Message(entity, this, "heal", 2);

Message cũng có một điểm đến (entity), một sender (this), đó là healer thực hiện hành động này, type của message ("heal") và điểm số cứu chữa (2, được gán vào field data của message).

Chúng ta bổ sung Healer vào danh sách thực thể trong PlayState giống với cách ta đã làm với Hunter và kết quả một bối cảnh gồm các runner, một hunter, và một healer:

Và xong! Chúng ta đã có 3 thực thể khác biệt tương tác với nhau bằng cách trao đổi message.

Thảo luận về tính linh hoạt

Hệ thống message queue là phương pháp linh hoạt để quản lý tương tác trong game. Tương tác được thực hiện thông qua một kênh giao tiếp thống nhất và có giao diện riêng lẻ dễ triển khai và dễ sử dụng.

Khi game của bạn phát triển phức tạp, có thể cần có tương tác mới. Một số có thể là ngoài ý muốn, vì thế bạn cần phải thích ứng code của mình để xử lý chúng. Nếu bạn đang dùng hệ thống message queue, đây là vấn đề của việc bổ sung một message mới ở đâu đó và xử lý nó trong một nơi khác.

Ví dụ, hình dung bạn muốn để Hunter tương tác với Healer, bạn chỉ phải để Hunter gửi message cùng một tương tác mới, ví dụ "flee" và bảo đảm rằng Healer có thử xử lý nó trong phương thức onMessage:

1
// In the Hunter class:

2
Hunter.prototype.someMethod = function() {
3
    // Get a reference to a nearby healer

4
    var healer = this.getNearbyHealer();
5
    
6
    // Create message about fleeing a place

7
    var place = {x: 30, y: 40};
8
    var msg = new Message(entity, this, "flee", place);
9
10
    // Send the message away!

11
    this.getMessageQueue().add(msg);
12
};
13
14
// In the Healer class:

15
Healer.prototype.onMessage = function(message) {
16
    if(message.type == "flee") {
17
        // Get the place to flee from the data field in the message

18
        var place = message.data;
19
        
20
        // Use the place information

21
        flee(place.x, place.y);
22
    }
23
};

Tại sao không trực tiếp gửi message đi?

Dù việc trao đổi message giữa các thực thể có thể hữu dụng, có lẽ bạn đang nghĩ tại sao cần có MessageQueue. Bạn không thể chỉ tự mình kích hoạt phương thức onMessage() thay vì dựa vào MessageQueue, như code bên dưới?

1
Hunter.prototype.someMethod = function() {
2
    // Get a reference to a nearby healer

3
    var healer = this.getNearbyHealer();
4
    
5
    // Create message about fleeing a place

6
    var place = {x: 30, y: 40};
7
    var msg = new Message(entity, this, "flee", place);
8
9
    // Bypass the MessageQueue and directly deliver

10
    // the message to the healer.

11
    healer.onMessage(msg);
12
};

Bạn chắc chắn có thể thực hiện một hệ thống message như thế, nhưng việc dùng một MessageQueue mang đến vài lợi thế.

Ví dụ, bằng cách tập trung gửi message, bạn có thể thực hiện một số tính năng thú vị như message bị trì hoãn, khả năng nhắn tin cho một nhóm thực thể và thông tin gỡ lỗi trực quan (chẳng hạn như flying envelope được sử dụng trong hướng dẫn này).

Vẫn có không gian để sáng tạo trong class MessageQueue, tùy thuộc vào bạn và yêu cầu trong trò chơi của bạn.

Tổng kết

Xử lý tương tác giữa các thực thể trong game bằng hệ thống message queue là phương pháp để giữ mã của bạn được sắp xếp và sẵn sàng cho tương lai. Tương tác mới có thể dễ dàng và nhanh chóng được bổ sung vào, ngay cả những ý tưởng phức tạp nhất của bạn, miễn là chúng được đóng gói dưới dạng message.

Như đã thảo luận trong hướng dẫn, bạn có thể bỏ qua việc dùng message queue và chỉ gửi message trực tiếp đến các thực thể. Bạn cũng có thể tập trung hoá việc giao tiếp bằng cách sử dụng class MessageQueue để dành chỗ cho các tính năng mới trong tương lai, chẳng hạn như message bị trì hoãn.

Tôi hy vọng phương pháp này hữu dụng và bổ sung vào các công cụ phát triển game của bạn. Phương pháp có lẽ quá mức cho các dự án nhỏ, nhưng chắc chắn nó sẽ giúp bạn bớt đau đầu trong các dự án game dài hơi.

Advertisement
Did you find this post useful?
Want a weekly email summary?
Subscribe below and we’ll send you a weekly email summary of all new Game Development tutorials. Never miss out on learning about the next big thing.
Advertisement
Looking for something to help kick start your next project?
Envato Market has a range of items for sale to help get you started.