Advertisement
  1. Code
  2. Coding Fundamentals
  3. Game Development

Làm thế nào để làm cho Roguelike đầu tiên của bạn

Scroll to top
Read Time: 15 min

() translation by (you can also view the original English article)

Roguelikes đã được chú ý gần đây, với các trò chơi như Dungeons của Dredmor, Spelunky, The Binding of Isaac, và FTL tiếp cận khán giả rộng lớn và nhận được sự ca ngợi quan trọng. Long rất thích những người chơi hardcore trong một niche nhỏ, các yếu tố roguelike trong các kết hợp khác nhau giờ đây giúp mang lại độ sâu và khả năng chơi lại cho nhiều thể loại hiện có.

Wayfarer a 3D roguelike currently in developmentWayfarer a 3D roguelike currently in developmentWayfarer a 3D roguelike currently in development
Wayfarer, một Roguelike 3D hiện đang được phát triển.

Trong hướng dẫn này, bạn sẽ học cách tạo ra một roguelike truyền thống bằng cách sử dụng JavaScript và công cụ trò chơi HTML 5 của Phaser. Đến cuối, bạn sẽ có một trò chơi roguelike đơn giản, đầy đủ chức năng, có thể chơi được trong trình duyệt của bạn! (Đối với mục đích của chúng tôi một roguelike truyền thống được định nghĩa là một trình thu thập dungeon đơn lẻ, ngẫu nhiên, theo lượt với permadeath.)

Click to play the game
Nhấp để chơi trò chơi.

Lưu ý: Mặc dù mã trong hướng dẫn này sử dụng JavaScript, HTML và Phaser, bạn sẽ có thể sử dụng cùng một kỹ thuật và khái niệm trong hầu hết các ngôn ngữ lập trình và công cụ trò chơi khác.


Sẵn sàng

Đối với hướng dẫn này, bạn sẽ cần một trình soạn thảo văn bản và trình duyệt. Tôi sử dụng Notepad ++ và tôi thích Google Chrome cho các công cụ dành cho nhà phát triển mở rộng của nó, nhưng quy trình làm việc sẽ khá giống với bất kỳ trình soạn thảo văn bản và trình duyệt nào bạn chọn.

Sau đó, bạn nên tải xuống các tệp nguồn và bắt đầu với thư mục init; điều này chứa Phaser và các tệp HTML và JS cơ bản cho trò chơi của chúng tôi. Chúng tôi sẽ viết mã trò chơi của chúng tôi trong tệp rl.js hiện đang trống.

Tệp index.html chỉ cần tải Phaser và tệp mã trò chơi nói trên của chúng tôi:

1
<!DOCTYPE html>
2
<head>
3
  <title>roguelike tutorial</title>
4
	<script src="phaser.min.js"></script>
5
	<script src="rl.js"></script>
6
</head>
7
</html>

Khởi tạo và định nghĩa

Hiện tại, chúng ta sẽ sử dụng đồ họa ASCII cho roguelike của mình - trong tương lai, chúng ta có thể thay thế chúng bằng đồ họa bitmap, nhưng hiện tại, việc sử dụng ASCII đơn giản làm cho cuộc sống của chúng ta dễ dàng hơn.

Hãy định nghĩa một số hằng số cho kích thước phông chữ, kích thước của bản đồ của chúng ta (có nghĩa là, mức độ), và bao nhiêu diễn viên sinh ra trong nó:

1
        // font size

2
        var FONT = 32;
3
4
        // map dimensions

5
        var ROWS = 10;
6
        var COLS = 15;
7
8
        // number of actors per level, including player

9
        var ACTORS = 10;

Chúng ta hãy khởi tạo Phaser và lắng nghe các sự kiện key-up bàn phím, vì chúng ta sẽ tạo một trò chơi theo lượt và sẽ hành động một lần cho mỗi cú đánh chính:

1
// initialize phaser, call create() once done

2
var game = new Phaser.Game(COLS * FONT * 0.6, ROWS * FONT, Phaser.AUTO, null, {
3
        create: create
4
});
5
6
function create() {
7
        // init keyboard commands

8
        game.input.keyboard.addCallbacks(null, null, onKeyUp);
9
}
10
11
function onKeyUp(event) {
12
        switch (event.keyCode) {
13
                case Keyboard.LEFT:
14
15
                case Keyboard.RIGHT:
16
17
                case Keyboard.UP:
18
19
                case Keyboard.DOWN:
20
21
        }
22
}

Kể từ khi phông chữ mặc định monospace có xu hướng khoảng 60% rộng khi chúng cao, chúng tôi đã khởi tạo kích thước canvas là 0,6 * kích thước phông chữ * số cột. Chúng tôi cũng nói với Phaser rằng nó nên gọi hàm create () của chúng ta ngay lập tức sau khi nó khởi tạo xong, lúc đó chúng ta khởi tạo các điều khiển bàn phím.

Bạn có thể xem các trò chơi cho đến nay - không phải là có nhiều để xem!


Bản đô

Bản đồ lát gạch đại diện cho khu vực chơi của chúng tôi: mảng lát gạch hoặc ô trống rời rạc (trái ngược với liên tục), mỗi ô được biểu thị bằng ký tự ASCII có thể biểu thị một bức tường (#: khối chuyển động) hoặc sàn (.: Không chuyển động khối):

1
        // the structure of the map

2
        var map;

Hãy sử dụng hình thức tạo thủ tục đơn giản nhất để tạo ra các bản đồ của chúng ta: quyết định ngẫu nhiên ô nào sẽ chứa một bức tường và một tầng nào đó:

1
function initMap() {
2
        // create a new random map

3
        map = [];
4
        for (var y = 0; y < ROWS; y++) {
5
                var newRow = [];
6
                for (var x = 0; x < COLS; x++) {
7
                     if (Math.random() > 0.8)
8
                        newRow.push('#');
9
                    else
10
                        newRow.push('.');
11
                }
12
                map.push(newRow);
13
        }
14
}

Điều này sẽ cho chúng ta một bản đồ nơi 80% của các tế bào là tường và phần còn lại là tầng.

Chúng tôi khởi tạo bản đồ mới cho trò chơi của chúng tôi trong hàm create (), ngay sau khi thiết lập trình nghe sự kiện bàn phím:

1
function create() {
2
        // init keyboard commands

3
        game.input.keyboard.addCallbacks(null, null, onKeyUp);
4
5
        // initialize map

6
        initMap();
7
}

Bạn có thể xem bản trình diễn tại đây — mặc dù, một lần nữa, không có gì để xem, vì chúng tôi chưa hiển thị bản đồ.


Màn hình

Đã đến lúc vẽ bản đồ của chúng ta! Màn hình của chúng tôi sẽ là một mảng các phần tử văn bản 2D, mỗi phần tử chứa một ký tự đơn:

1
        // the ascii display, as a 2d array of characters

2
        var asciidisplay;

Vẽ bản đồ sẽ điền vào nội dung của màn hình với các giá trị của bản đồ vì cả hai đều là các ký tự ASCII đơn giản:

1
        function drawMap() {
2
            for (var y = 0; y < ROWS; y++)
3
                for (var x = 0; x < COLS; x++)
4
                    asciidisplay[y][x].content = map[y][x];
5
        }

Cuối cùng, trước khi vẽ bản đồ, chúng ta phải khởi tạo màn hình. Chúng ta quay lại hàm create () của chúng ta:

1
        function create() {
2
                // init keyboard commands

3
                game.input.keyboard.addCallbacks(null, null, onKeyUp);
4
5
                // initialize map

6
                initMap();
7
8
                // initialize screen

9
                asciidisplay = [];
10
                for (var y = 0; y < ROWS; y++) {
11
                        var newRow = [];
12
                        asciidisplay.push(newRow);
13
                        for (var x = 0; x < COLS; x++)
14
                                newRow.push( initCell('', x, y) );
15
                }
16
                drawMap();
17
        }
18
19
        function initCell(chr, x, y) {
20
                // add a single cell in a given position to the ascii display

21
                var style = { font: FONT + "px monospace", fill:"#fff"};
22
                return game.add.text(FONT*0.6*x, FONT*y, chr, style);
23
        }

Bây giờ bạn sẽ thấy một bản đồ ngẫu nhiên được hiển thị khi bạn chạy dự án.

Click to view the game so far
Nhấn vào đây để xem các trò chơi cho đến nay.

Diễn viên

Tiếp theo trong dòng là các diễn viên: nhân vật người chơi của chúng tôi, và những kẻ thù họ phải đánh bại. Mỗi diễn viên sẽ là một đối tượng có ba trường: x và y cho vị trí của nó trên bản đồ và hp cho điểm nhấn của nó.

Chúng tôi giữ tất cả các tác nhân trong mảng actorList (phần tử đầu tiên trong số đó là trình phát). Chúng tôi cũng giữ một mảng kết hợp với các vị trí của các tác nhân như các phím để tìm kiếm nhanh, để chúng tôi không phải lặp qua toàn bộ danh sách diễn viên để tìm diễn viên nào chiếm một vị trí nhất định; điều này sẽ giúp chúng ta khi chúng ta mã hóa phong trào và chiến đấu.

1
// a list of all actors; 0 is the player

2
var player;
3
var actorList;
4
var livingEnemies;
5
6
// points to each actor in its position, for quick searching

7
var actorMap;

Chúng tôi tạo tất cả các diễn viên của chúng tôi và chỉ định một vị trí miễn phí ngẫu nhiên trong bản đồ cho mỗi người:

1
function randomInt(max) {
2
   return Math.floor(Math.random() * max);
3
}
4
5
function initActors() {
6
        // create actors at random locations

7
        actorList = [];
8
        actorMap = {};
9
        for (var e=0; e<ACTORS; e++) {
10
                // create new actor

11
                var actor = { x:0, y:0, hp:e == 0?3:1 };
12
                do {
13
                        // pick a random position that is both a floor and not occupied

14
                        actor.y=randomInt(ROWS);
15
                        actor.x=randomInt(COLS);
16
                } while ( map[actor.y][actor.x] == '#' || actorMap[actor.y + "_" + actor.x] != null );
17
18
                // add references to the actor to the actors list & map

19
                actorMap[actor.y + "_" + actor.x]= actor;
20
                actorList.push(actor);
21
        }
22
23
        // the player is the first actor in the list

24
        player = actorList[0];
25
        livingEnemies = ACTORS-1;
26
}

Đã đến lúc thể hiện diễn viên! Chúng ta sẽ vẽ tất cả kẻ thù là e và nhân vật của người chơi như số lượng các điểm đánh của nó:

1
function drawActors() {
2
        for (var a in actorList) {
3
                if (actorList[a].hp > 0)
4
                        asciidisplay[actorList[a].y][actorList[a].x].content = a == 0?''+player.hp:'e';
5
        }
6
}

Chúng ta sử dụng các hàm mà chúng ta vừa viết để khởi tạo và vẽ tất cả các tác nhân trong hàm create () của chúng ta:

1
function create() {
2
	...
3
	// initialize actors

4
	initActors();
5
	...
6
	drawActors();
7
}

Bây giờ chúng ta có thể thấy nhân vật và kẻ thù của chúng ta trải rộng ở cấp độ!

Click to view the game so far
Nhấn vào đây để xem các trò chơi cho đến nay.

Gạch chặn và có thể truy cập

Chúng ta cần đảm bảo rằng các diễn viên của chúng ta không chạy khỏi màn hình và thông qua các bức tường, vì vậy hãy thêm kiểm tra đơn giản này để xem các hướng mà một diễn viên cụ thể có thể đi bộ:

1
function canGo(actor,dir) {
2
	return 	actor.x+dir.x >= 0 &&
3
		actor.x+dir.x <= COLS - 1 &&
4
                actor.y+dir.y >= 0 &&
5
		actor.y+dir.y <= ROWS - 1 &&
6
		map[actor.y+dir.y][actor.x +dir.x] == '.';
7
}

Phong trào và chiến đấu

Chúng tôi cuối cùng đã đến một số tương tác: phong trào và chiến đấu! Vì, trong roguelikes cổ điển, tấn công cơ bản được kích hoạt bằng cách chuyển sang một diễn viên khác, chúng ta xử lý cả hai cùng một điểm, hàm moveTo () của chúng ta, lấy một diễn viên và một hướng (hướng là sự khác biệt mong muốn trong x và y đến vị trí mà diễn viên bước vào):

1
function moveTo(actor, dir) {
2
        // check if actor can move in the given direction

3
        if (!canGo(actor,dir))
4
                return false;
5
6
        // moves actor to the new location

7
        var newKey = (actor.y + dir.y) +'_' + (actor.x + dir.x);
8
        // if the destination tile has an actor in it

9
        if (actorMap[newKey] != null) {
10
                //decrement hitpoints of the actor at the destination tile

11
                var victim = actorMap[newKey];
12
                victim.hp--;
13
14
                // if it's dead remove its reference

15
                if (victim.hp == 0) {
16
                        actorMap[newKey]= null;
17
                        actorList[actorList.indexOf(victim)]=null;
18
                        if(victim!=player) {
19
                                livingEnemies--;
20
                                if (livingEnemies == 0) {
21
                                        // victory message

22
                                        var victory = game.add.text(game.world.centerX, game.world.centerY, 'Victory!\nCtrl+r to restart', { fill : '#2e2', align: "center" } );
23
                                        victory.anchor.setTo(0.5,0.5);
24
                                }
25
                        }
26
                }
27
        } else {
28
                // remove reference to the actor's old position

29
                actorMap[actor.y + '_' + actor.x]= null;
30
31
                // update position

32
                actor.y+=dir.y;
33
                actor.x+=dir.x;
34
35
                // add reference to the actor's new position

36
                actorMap[actor.y + '_' + actor.x]=actor;
37
        }
38
        return true;
39
}

Về cơ bản:

  1. Chúng tôi đảm bảo rằng diễn viên đang cố gắng chuyển sang vị trí hợp lệ.
  2. Nếu có một diễn viên khác ở vị trí đó, chúng tôi tấn công nó (và giết nó nếu số HP của nó đạt 0).
  3. Nếu không có một diễn viên khác ở vị trí mới, chúng tôi sẽ chuyển đến đó.

Lưu ý rằng chúng tôi cũng hiển thị thông điệp chiến thắng đơn giản khi kẻ thù cuối cùng bị giết và trả về false hoặc true tùy thuộc vào việc chúng tôi có thực hiện hành động hợp lệ hay không.

Bây giờ, chúng ta hãy quay lại hàm onKeyUp () của chúng ta và thay đổi nó để mỗi khi người dùng nhấn một phím, chúng ta xóa vị trí của diễn viên trước đó khỏi màn hình (bằng cách vẽ bản đồ lên trên), di chuyển nhân vật đến vị trí, và sau đó vẽ lại các diễn viên:

1
function onKeyUp(event) {
2
        // draw map to overwrite previous actors positions

3
        drawMap();
4
5
        // act on player input

6
        var acted = false;
7
        switch (event.keyCode) {
8
                case Phaser.Keyboard.LEFT:
9
                        acted = moveTo(player, {x:-1, y:0});
10
                        break;
11
12
                case Phaser.Keyboard.RIGHT:
13
                        acted = moveTo(player,{x:1, y:0});
14
                        break;
15
16
                case Phaser.Keyboard.UP:
17
                        acted = moveTo(player, {x:0, y:-1});
18
                        break;
19
20
                case Phaser.Keyboard.DOWN:
21
                        acted = moveTo(player, {x:0, y:1});
22
                        break;
23
        }
24
25
        // draw actors in new positions

26
        drawActors();
27
}

Chúng ta sẽ sớm sử dụng biến hành động để biết kẻ thù có nên hành động sau mỗi lần đầu vào của người chơi hay không.

Click to view the game so far
Nhấn vào đây để xem các trò chơi cho đến nay.

Trí tuệ nhân tạo cơ bản

Bây giờ nhân vật người chơi của chúng tôi đang di chuyển và tấn công, hãy thậm chí là tỷ lệ cược bằng cách làm cho kẻ thù hành động theo cách tìm đường rất đơn giản miễn là người chơi có sáu bước hoặc ít hơn từ họ. (Nếu người chơi ở xa hơn, kẻ địch sẽ đi ngẫu nhiên.)

Lưu ý rằng mã tấn công của chúng tôi không quan tâm ai là diễn viên đang tấn công; điều này có nghĩa rằng, nếu bạn sắp xếp chúng vừa phải, kẻ thù sẽ tấn công lẫn nhau trong khi cố gắng theo đuổi nhân vật người chơi, Doom-phong cách!

1
function aiAct(actor) {
2
        var directions = [ { x: -1, y:0 }, { x:1, y:0 }, { x:0, y: -1 }, { x:0, y:1 } ];
3
        var dx = player.x - actor.x;
4
        var dy = player.y - actor.y;
5
6
        // if player is far away, walk randomly

7
        if (Math.abs(dx) + Math.abs(dy) > 6)
8
                // try to walk in random directions until you succeed once

9
                while (!moveTo(actor, directions[randomInt(directions.length)])) { };
10
11
        // otherwise walk towards player

12
        if (Math.abs(dx) > Math.abs(dy)) {
13
                if (dx < 0) {
14
                        // left

15
                        moveTo(actor, directions[0]);
16
                } else {
17
                        // right

18
                        moveTo(actor, directions[1]);
19
                }
20
        } else {
21
                if (dy < 0) {
22
                        // up

23
                        moveTo(actor, directions[2]);
24
                } else {
25
                        // down

26
                        moveTo(actor, directions[3]);
27
                }
28
        }
29
        if (player.hp < 1) {
30
                // game over message

31
                var gameOver = game.add.text(game.world.centerX, game.world.centerY, 'Game Over\nCtrl+r to restart', { fill : '#e22', align: "center" } );
32
                gameOver.anchor.setTo(0.5,0.5);
33
        }
34
}

Chúng tôi cũng đã thêm một trò chơi qua tin nhắn, được hiển thị nếu một trong những kẻ thù giết người chơi.

Bây giờ tất cả những gì còn lại là làm cho kẻ thù hành động mỗi khi người chơi di chuyển, điều này đòi hỏi phải thêm những điều sau vào cuối các hàm onKeyUp () của chúng ta, ngay trước khi vẽ các diễn viên ở vị trí mới của họ:

1
function onKeyUp(event) {
2
        ...
3
        // enemies act every time the player does

4
        if (acted)
5
                for (var enemy in actorList) {
6
                        // skip the player

7
                        if(enemy==0)
8
                                continue;
9
10
                        var e = actorList[enemy];
11
                        if (e != null)
12
                                aiAct(e);
13
                }
14
15
        // draw actors in new positions

16
        drawActors();
17
}
Click to view the game so far
Nhấn vào đây để xem các trò chơi cho đến nay.

Tiền thưởng: Phiên bản Haxe

Ban đầu tôi đã viết hướng dẫn này trong Haxe, một ngôn ngữ đa nền tảng tuyệt vời biên dịch sang JavaScript (trong số các ngôn ngữ khác). Mặc dù tôi đã dịch phiên bản trên bằng tay để đảm bảo rằng chúng tôi nhận được JavaScript theo phong cách riêng, nếu như tôi thích Haxe hơn với JavaScript, bạn có thể tìm thấy phiên bản Haxe trong thư mục haxe của tệp tải xuống nguồn.

Trước tiên, bạn cần cài đặt trình biên dịch haxe và có thể sử dụng bất kỳ trình soạn thảo văn bản nào bạn muốn và biên dịch mã haxe bằng cách gọi haxe build.hxml hoặc nhấp đúp vào tệp build.hxml. Tôi cũng đưa vào một dự án FlashDevelop nếu bạn thích một IDE tốt đẹp cho một trình soạn thảo văn bản và dòng lệnh; chỉ cần mở rl.hxproj và nhấn F5 để chạy.


Tóm lược

Đó là nó! Bây giờ chúng ta có một roguelike đơn giản hoàn chỉnh, với việc tạo bản đồ ngẫu nhiên, chuyển động, chiến đấu, AI và cả hai điều kiện thắng và thua.

Dưới đây là một số ý tưởng cho các tính năng mới mà bạn có thể thêm vào trò chơi của mình:

  • nhiều cấp độ
  • tăng sức mạnh
  • hàng tồn kho
  • hàng tiêu dùng
  • Trang thiết bị

Hãy thưởng thức!

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 Code 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.