Advertisement
  1. Game Development
  2. Roguelike

Bagaimana cara membuat game Roguelike-mu yang pertama

Scroll to top
Read Time: 12 min

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

Roguelike belakangan kembali menjadi sorotan, dengan kemunculan game seperti Dungeon of Dredmor, Spelunky, The Binding of Isaac, and FTL yang dimainkan banyak orang dan dinilai cukup sukses. Dahulu elemen roguelike hanya dinikmati oleh kelompok pemain hardcore, sekarang sudah diserap oleh berbagai game untuk menambah kedalaman game dan nilai untuk dimainkan ulang.

Wayfarer a 3D roguelike currently in developmentWayfarer a 3D roguelike currently in developmentWayfarer a 3D roguelike currently in development
Wayfarer, sebuah game roguelike yang sedang dikembangkan.

Pada tutorial ini kamu akan belajar bagaimana cara membuat game roguelike tradisional menggunakan JavaScript dan game engine HTML5 Phaser. Di akhir tutorial, kamu akan memiliki sebuah game roguelike sederhana yang bisa dimainkan di browser! (Dalam tutorial ini, game roguelike sederhana didefinisikan sebagai game satu pemain, dengan level yang diacak, dungeon-crawler bergiliran dengan fitur permadeath.

Click to play the game
Klik untuk memainkan game.
Artikel yang bersangkutan

Catatan: Walaupun kode pada tutorial ini menggunakan JavaScript, HTML, dan Phaser, kamu bisa menggunakan teknik dan konsep yang sama di bahasa pemrograman atau game engine lain.


Memulai

Untuk tutorial ini, kamu membutuhkan sebuah editor teks dan browser. Saya menggunakan Notepad++, dan Google Chrome karena developer tools-nya, tapi alur kerjanya kurang lebih sama dengan menggunakan editor teks dan browser lain.

Sekarang kamu perlu mengunduh file sumber, dan mulai dengan folder init, yang berisi Phaser dan file dasar HTML dan JS untuk game kita. Kita akan menulis kode game kita pada file rl.js yang saat ini masih kosong.

File index.html hanya memuat Phaser dan file kode game yang disebutkan sebelumnya:

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>

Inisialisasi dan Definisi

Sementara, kita akan menggunakan grafik ASCII untuk game roguelike kita agar memudahkan proses pengembangan game, nantinya, kita bisa mengubah grafik ini dengan file bitmap. 

Sekarang kita akan menentukan ukuran font, dimensi denah (level ini), dan berapa aktor yang muncul di denah:

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;

Inisialisasi Phaser, dan kita perlu terima event key-up, karena kita akan membuat turn-based game karena kita ingin bisa beraksi untuk setiap tombol yang ditekan.

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
}

Karena font monospace biasanya memiliki lebar 60% dari tingginya, kita mengatur ukuran canvas menjadi 0.6 * ukuran font * jumlah kolom. Kita juga memberitahu Phaser untuk memanggil fungsi create() begitu Phaser selesai inisialisasi, yang akan kita gunakan untuk menginisialisasi keyboard.

Kamu bisa melihat game sejauh ini, belum banyak yang bisa dilihat!


Denah Permainan

Denah petak akan menggambarkan area bermain kita: sejumlah petak pada 2D array, disebut juga sel, masing-masing diwakilkan dengan karakter ASCII yang mewakili tembok (#: tidak bisa ditembus) atau lantai (.: bisa ditembus)

1
        // the structure of the map

2
        var map;

Kita akan menggunakan bentuk paling sederhana dari procedural generation untuk membuat map: menentukan mana sel yang berisi tembok dan mana yang lantai:

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
}

Kode berikut akan memberi kita denah dengan 80% sel adalah tembok dan sisanya berupa lantai.

Kita membuat denah baru untuk game kita pada fungsi create(), tepat setelah menyiapkan event listener untuk keyboard:

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

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

6
        initMap();
7
}

Kamu bisa melihat demonya di sini, walau saat ini belum ada yang bisa dilihat, karena kita belum menampilkan denahnya ke layar.


Fitur Layar

Saatnya menggambar denah kita! Layar kita akan menjadi array 2D yang berisi elemen teks, masing-masing berisi satu karakter.

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

2
        var asciidisplay;

Menggambar denah akan mengisi layar dengan isi dari nilai denah, yang berupa karakter ASCII sederhana.

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
        }

Akhirnya, sebelum kita menggambar denah kita perlu menginisialisasi layar. Kita kembali ke fungsi create():

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
        }

Kamu sekarang sudah bisa melihat denah acak saat project dijalankan.

Click to view the game so far
Klik di sini untuk melihat game sejauh ini.

Aktor

Berikutnya kita perlu membuat aktor dalam game: karakter pemain, dan musuh ang harus dikalahkan pemain. Masing-masing aktor adalah objek dengan tiga nilai: x dan y untuk lokasinya pada denah, dan hp untuk nyawa.

Kita akan simpan semua aktor di array actorList (dengan elemen pertama adalah pemain). Kita juga menyimpan sebuah associative array yang berisi lokasi aktor sebagai kunci untuk pencarian cepat, jadi kita tidak perlu memeriksa keseluruhan daftar aktor untuk mengetahui aktor mana mengisi lokasi tertentu; ini akan membantu kita saat membuat kode untuk pergerakan dan pertarungan.

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;

Kita buat semua aktor dan menentukan posisi acak pada denah yang kosong untuk masing-masing aktor:

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
}

Sekarang saatnya untuk menampilkan aktor! Kita akan menggambar semua musuh sebagai e dan pemain digambar sesuai dengan sisa nyawa yang dimilikinya:

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
}

Kita akan menggunakan fungsi yang kita buat untuk menginisialisasi dan menggambar semua aktor dalam fungsi create():

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

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

Kita sekarang bisa melihat karakter pemain dan musuh menyebar dalam level ini!

Click to view the game so far
Klik untuk melihat game sejauh ini.

Petak yang bisa dilewati dan yang tidak bisa ditembus

Kita perlu memastikan bahwa aktor kita tidak berjalan keluar layar dan menembus tembok, jadi kita tambahkan pengecekan sederhana untuk mengetahui arah mana aktor bisa berjalan:

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
}

Pergerakan dan Pertarungan

Kita akhirnya perlu membuat interaksi dalam game: pergerakan dan pertarungan! Seperti pada game roguelike klasik, serangan akan dipicu dengan bergerak menuju aktor lain, ini akan kita tangani pada bagian yang sama, yaitu fungsi moveTo(). Fungsi ini menerima sebuah aktor dan arah (yang ditentukan dengan selisih x dan y dari posisi aktor yang akan bergerak).

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
}

Pada dasarnya:

  1. Kita perlu memastikan aktor bergerak menuju posisi yang valid.
  2. Jika ada aktor lain di posisi tersebut, kita akan menyerangnya (dan mengalahkannya jika nyawa nya mencapai 0)
  3. Jika tidak ada aktor lain di posisi tersebut, kita bergerak ke sana.

Perhatikan bahwa kita juga perlu menampilkan pesan kemenangan sederhana begitu semua musuh sudah dikalahkan, dan mengembalikan false atau true tergantung berhasil tidaknya kita bergerak.

Sekarang, kita kembali ke fungsi onKeyUp() dan mengubahnya agar setiap pengguna menekan tombol, kita menghapus posisi aktor sebelumnya dari layar (dengan menggambar denah di atasnya), pindahkan karakter pemain ke lokasi yang baru, dan gambar ulang aktor tersebut:

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
}

Kita akan gunakan variabel acted untuk mengetahui apakah musuh-musuh perlu bergerak setelah input pemain.

Click to view the game so far
Klik untuk melihat game sejauh ini.

Intelenjensi Buatan Dasar

Sekarang karakter pemain kita sudah bisa bergerak dan menyerang, agar lebih adil, kita buat musuh bisa bergerak menuju pemain dengan teknik pathfinding sederhana selama pemain berjarak enam langkah atau kurang dari musuh yang bersangkutan. (Jika pemain lebih jauh, musuh bergerak secara acak).

Perhatikan bahwa kode penyerangan kita tidak peduli siapa aktor yang menyerang, artinya jika posisinya tepat, musuh akan saling meyerang sesamanya saat berusaha mengejar karakter pemain, seperti pada game Doom.

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
}

Kita juga perlu menambahkan pesan game over, yang akan dimunculkan jika salah satu musuh berhasil mengalahkan pemain.

Sekarang yang perlu kita lakukan adalah membuat musuh beraksi setiap pemain bergerak, yang membutuhkan kita menambahkan kode berikut di akhir fungsi onKeyUp(), tepat sebelum menggambar aktor di posisi barunya:

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
Klik untuk melihat game sejauh ini.

Bonus: Versi Haxe

Awalnya saya menulis tutorial ini dalam Haxe, bahasa pemrograman multi-platform yang bisa dicompile ke Javascript (dan bahasa lainnya). Walaupun saya membuat versi di atas dari awal untuk memastikan kode JavaScript yang berbeda, tapi jika lebih memilih Haxe dibanding JavaScript, kamu bisa menemukan versi Haxe di folder haxe di folder source code yang bisa didownload.

Kamu perlu menginstall haxe compiler dan menggunakan editor teks yang kamu inginkan, dan mengompile kode haxe dengan menjalankan perintah haxe build.hxml atau klik ganda file build.hxml. Saya juga lampirkan project FlashDevelop jika kamu ingin IDE dibanding editor teks dan command line; cukup buka rl.hxproj dan tekan F5 untuk menjalankan game.


Ringkasan

Selesai sudah! Sekarang kita sudah punya game roguelike sederhana, dengan denah yang dihasilkan secara acak, pergerakan, pertarungan, intelejensi buatan dan kondisi menang kalah.

Berikut adalah beberapa ide untuk fitur baru yang bisa kamu tambahkan ke dalam game-mu:

  • Lebih dari satu level
  • Power Up
  • Inventory
  • Item yang bisa dikonsumsi
  • Equipment

Selamat menikmati!

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.