Bagaimana cara membuat game Roguelike-mu yang pertama
() 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.



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.

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.

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!

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:
- Kita perlu memastikan aktor bergerak menuju posisi yang valid.
- Jika ada aktor lain di posisi tersebut, kita akan menyerangnya (dan mengalahkannya jika nyawa nya mencapai 0)
- 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.

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 |
}
|

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!