Fundamental raylib

Jendela, game loop, dan input dasar.

Daftar isi

Topik ini membahas fondasi program grafika dengan raylib pada Odin: membuka dan menutup jendela, menyusun game loop, yaitu perulangan utama program berjendela, serta membaca input keyboard. Ketiga hal tersebut dirangkai menjadi satu program contoh bernama Kotak Jalan yang dikompilasi, dijalankan, dan diperiksa perilakunya di lingkungan tanpa tampilan (headless). Setelah topik ini selesai dibaca, mekanisme di balik jendela dan game loop dapat dijelaskan baris demi baris, dan program contohnya dapat dimodifikasi untuk keperluan sendiri.

Jendela dan siklus hidup program#

Seluruh program raylib dimulai dari pemanggilan rl.InitWindow(lebar, tinggi, judul). Proc ini membuka jendela sekaligus konteks OpenGL yang menjadi latar seluruh penggambarannya, sehingga pemanggilannya harus terjadi sebelum proc raylib lain yang menyangkut tampilan. Tiga argumennya menentukan lebar dan tinggi jendela dalam piksel serta judul yang tampil pada bilah jendela. Di akhir program, rl.CloseWindow() menutup jendela dan membebaskan seluruh sumber daya yang menyertainya. Pustaka diimpor dengan menulis import rl "vendor:raylib", dan alias rl menjadi awalan setiap pemanggilan proc raylib.

Pembukaan jendela dapat gagal, misalnya ketika program dijalankan pada mesin yang tidak menjalankan server grafika atau pada lingkungan yang tidak memiliki variabel lingkungan DISPLAY. Proc rl.IsWindowReady() membaca keadaan tersebut dan mengembalikan true hanya bila inisialisasi berhasil. Memeriksa nilai kembaliannya sebelum memasuki game loop membuat program berperilaku wajar di kedua lingkungan: pada desktop, jendela terbuka dan program berjalan, sedangkan pada lingkungan tanpa tampilan, program berhenti dengan pesan yang menjelaskan penyebabnya.

Selama jendela terbuka, satu proc menjadi penentu akhir program, yaitu rl.WindowShouldClose(). Proc ini mengembalikan true ketika pengguna menekan tombol ESC atau menutup jendela melalui ikonnya. Karena itulah game loop pada bagian berikutnya selalu diberi syarat for !rl.WindowShouldClose(), sehingga perulangan berhenti tepat ketika pengguna menghendakinya.

Game loop dan pembuatan frame#

Program berjendela tidak berjalan dari atas ke bawah lalu berhenti, melainkan berputar dalam perulangan yang disebut game loop. Setiap putaran disebut frame, dan jumlah frame per detik menjadi ukuran kehalusan gerakan yang tampak di layar. Setiap frame menjalankan tiga fase dengan urutan tetap: memeriksa input, memperbarui kondisi program, dan menggambar hasilnya ke layar. Urutan ini membuat tombol yang ditekan pengguna diterjemahkan menjadi perubahan kondisi sebelum layar digambar, sehingga yang tampak selalu merupakan keadaan terbaru.

Fase menggambar selalu diapit oleh dua proc yang berpasangan. rl.BeginDrawing() memasuki mode kanvas tempat seluruh pemanggilan proc menggambar seperti rl.DrawText dan rl.DrawRectangle dituliskan. rl.EndDrawing() menutup kanvas, mengakhiri frame, lalu menukar dua buffer tampilan dengan teknik yang disebut double buffering, sehingga frame yang sedang disusun tidak pernah terlihat setengah jadi. Pada awal frame, rl.ClearBackground(warna) mengisi seluruh layar dengan satu warna agar gambar frame sebelumnya tidak menumpuk. Proc rl.SetTargetFPS(60) menetapkan laju maksimum perulangan sehingga kecepatan program stabil pada angka yang ditetapkan, bukan mengikuti seberapa cepat perangkat menjalankan kodenya. Kerangka game loop raylib seluruhnya hanya berupa beberapa baris berikut.

odin Kerangka game loop raylib
// tidak-dijalankan
rl.SetTargetFPS(60)

for !rl.WindowShouldClose() {
	// 1. periksa input
	// 2. perbarui kondisi
	// 3. gambar frame
}

Ketiga fase dituliskan sebagai komentar karena isinya berbeda pada setiap program, sedangkan bentuk perulangannya tetap sama. Kode di dalam kerangka tersebut berada di dalam proc utuh setelah jendela siap, dan program contoh pada bagian berikutnya mengisinya secara nyata.

Program contoh: Kotak Jalan#

Program contoh pada topik ini menggerakkan sebuah kotak di dalam jendela 480×320 piksel. Kotak berukuran 40 piksel mulai dari koordinat x 40 dan bergeser dua piksel ke kanan pada setiap frame selama tombol panah kanan ditahan. Tombol SPASI mengembalikan posisi kotak ke titik awal. Program berhenti ketika pengguna menekan ESC, ketika jendela ditutup melalui ikonnya, atau setelah 300 frame terlewati, sehingga demo berhenti sendiri dan tidak perlu ditutup paksa ketika dicoba.

Dua hal berikut layak dicermati sebelum kodenya dibaca. Pemeriksaan rl.IsWindowReady() diletakkan tepat setelah rl.InitWindow, dan defer rl.CloseWindow() baru dipasang setelah jendela dipastikan siap. Variabel posisi dideklarasikan bertipe c.int karena argumen posisi pada binding raylib, yaitu deklarasi proc Odin yang menjembatani pustaka C-nya, memang bertipe tersebut, bukan int milik Odin.

odin main.odin
package main

import "core:c"
import "core:fmt"
import rl "vendor:raylib"

FRAME_MAKS :: 300

main :: proc() {
	rl.InitWindow(480, 320, "Kotak Jalan")
	if !rl.IsWindowReady() {
		fmt.println("Jendela tidak dapat dibuka: lingkungan tanpa tampilan.")
		fmt.println("Program berhenti dengan rapi tanpa memasuki game loop.")
		return
	}
	defer rl.CloseWindow()

	rl.SetTargetFPS(60)

	x: c.int = 40
	frame: int = 0

	for !rl.WindowShouldClose() {
		// 1. periksa input
		if rl.IsKeyDown(.RIGHT) {
			x += 2
		}
		if rl.IsKeyPressed(.SPACE) {
			x = 40
			fmt.println("SPASI ditekan: kotak kembali ke titik awal")
		}

		// 2. perbarui kondisi
		frame += 1
		if frame == FRAME_MAKS {
			break
		}

		// 3. gambar frame
		rl.BeginDrawing()
		rl.ClearBackground(rl.RAYWHITE)
		rl.DrawRectangle(x, 140, 40, 40, rl.MAROON)
		rl.DrawText("Kotak Jalan", 16, 16, 24, rl.DARKBLUE)
		rl.EndDrawing()
	}

	fmt.printfln("Selesai pada frame %d dengan posisi x = %v", frame, x)
}

Tipe dan pemanggilan program diperiksa terlebih dahulu, kemudian program dijalankan dari direktori topik.

bash
# memeriksa tipe dan pemanggilan binding tanpa menghasilkan berkas
odin check .
bash
# mengompilasi dan menjalankan program (butuh tampilan)
odin run main.odin -file
text Keluaran
INFO: Initializing raylib 6.0
INFO: Platform backend: DESKTOP (GLFW)
INFO: Supported raylib modules:
INFO:     > rcore:..... loaded (mandatory)
INFO:     > rlgl:...... loaded (mandatory)
INFO:     > rshapes:... loaded (optional)
INFO:     > rtextures:. loaded (optional)
INFO:     > rtext:..... loaded (optional)
INFO:     > rmodels:... loaded (optional)
INFO:     > raudio:.... loaded (optional)
WARNING: GLFW: Error: 65550 Description: X11: The DISPLAY environment variable is missing
WARNING: GLFW: Failed to initialize GLFW
WARNING: SYSTEM: Failed to initialize platform
Jendela tidak dapat dibuka: lingkungan tanpa tampilan.
Program berhenti dengan rapi tanpa memasuki game loop.

Keluaran di atas diambil dari sandbox yang tidak memiliki tampilan, sehingga prosesnya berhenti di titik pemeriksaan yang sudah disiapkan. Baris INFO adalah laporan inisialisasi raylib 6.0 beserta modul-modulnya, sedangkan baris WARNING menjelaskan penyebab kegagalannya, yaitu variabel lingkungan DISPLAY tidak ada sehingga pustaka GLFW gagal dimulai. Pemeriksaan rl.IsWindowReady() menangkap keadaan itu, program mencetak dua baris pesannya, lalu berakhir dengan kode keluar 0 tanpa error. Pada komputer desktop yang memiliki tampilan, laporan inisialisasi yang sama muncul, diikuti jendela yang terbuka dan kotak yang bergerak sesuai tombol yang ditekan. Ketika program berhenti, baris terakhir keluaran berisi nomor frame dan posisi x.

Input keyboard: IsKeyDown dan IsKeyPressed#

Binding raylib menyediakan dua proc pembacaan tombol yang perilakunya berbeda. rl.IsKeyDown(tombol) mengembalikan true selama tombol ditahan, sehingga nilainya tetap true pada setiap frame ketika tombol masih ditekan. rl.IsKeyPressed(tombol) mengembalikan true hanya pada frame pertama sejak tombol ditekan dan kembali false pada frame-frame berikutnya meskipun tombol masih ditahan. Perbedaan ini menentukan pilihan di dalam game loop: gerakan yang berlanjut, seperti kotak pada program contoh, dibaca dengan IsKeyDown, sedangkan aksi yang cukup dilakukan satu kali, seperti mengembalikan posisi, dibaca dengan IsKeyPressed.

ProcNilai kembalianPenggunaan yang cocok
rl.IsKeyDown(.RIGHT)true selama tombol ditahangerakan berlanjut per frame
rl.IsKeyPressed(.SPACE)true hanya pada frame pertama ditekanaksi satu kali

Argumen tombol ditulis sebagai anggota enum KeyboardKey dari paket vendor, contohnya .RIGHT, .SPACE, dan .ESCAPE. Binding Odin memangkas awalan KEY_ yang dipakai dokumentasi C raylib, sehingga KEY_RIGHT pada C menjadi .RIGHT pada Odin. Anggota lain mengikuti pola yang sama, misalnya .ENTER, .UP, dan huruf .A hingga .Z. Keadaan input itu sendiri diperbarui oleh raylib setiap rl.EndDrawing() dipanggil, sehingga pembacaan input cukup dilakukan sekali per frame pada fase pertama loop.

Kesalahan umum#

Dua kesalahan berikut sering muncul ketika program raylib pertama kali disusun dengan Odin. Kesalahan pertama tertangkap compiler sebelum program dijalankan, sedangkan kesalahan kedua hanya terlihat saat program berjalan di lingkungan tanpa tampilan.

Posisi bertipe int diteruskan ke proc menggambar#

Compiler menolak nilai bertipe int yang diteruskan ke argumen posisi karena binding raylib menuliskannya bertipe c.int, yaitu tipe bilangan bulat dengan lebar tetap milik antarmuka C. Program uji kecil berikut memuat kesalahan tersebut, diperiksa dengan odin check, lalu dihapus.

bash
# membuat program uji dengan posisi bertipe int
mkdir uji-tipe

cat > uji-tipe/main.odin <<'EOF'
package main

import rl "vendor:raylib"

main :: proc() {
	x: int = 40
	rl.DrawRectangle(x, 140, 40, 40, rl.MAROON)
}
EOF

# memeriksa program uji, lalu menghapusnya
odin check uji-tipe
rm -r uji-tipe
text Keluaran
/home/kal/my-project/content/odin/grafika-raylib/fundamental-raylib/uji-tipe/main.odin(7:19) Error: Cannot assign value 'x' of type 'int' to 'i32' in a procedure argument
	rl.DrawRectangle(x, 140, 40, 40, rl.MAROON)
	                 ^

Perbaikannya adalah mendeklarasikan variabel posisi bertipe c.int sebagaimana program contoh, atau mengonversi nilainya saat pemanggilan dengan c.int(x). Angka literal seperti 140 tidak memerlukan konversi karena compiler mengerjakannya secara otomatis.

defer rl.CloseWindow() yang dipasang tepat setelah rl.InitWindow tetap dijalankan ketika inisialisasi jendela gagal, karena defer tidak memandang keberhasilan proc sebelumnya. Proc rl.CloseWindow kemudian berupaya menutup jendela yang tidak pernah ada, dan program berakhir dengan segmentation fault yang dilaporkan shell sebagai kode keluar 139. Perbaikannya sudah dipraktikkan pada program contoh, yaitu memeriksa rl.IsWindowReady() terlebih dahulu dan keluar dengan return sebelum defer dipasang, sehingga penutupan jendela hanya dijadwalkan ketika jendelanya benar-benar terbuka.

Ringkasan#

  • rl.InitWindow membuka jendela beserta konteks gambar, rl.CloseWindow menutupnya, dan rl.IsWindowReady() membedakan keberhasilan inisialisasi di lingkungan dengan maupun tanpa tampilan.
  • Game loop menjalankan tiga fase per frame, yaitu memeriksa input, memperbarui kondisi, dan menggambar, dengan fase menggambar diapit rl.BeginDrawing dan rl.EndDrawing serta rl.ClearBackground di awal fase.
  • rl.WindowShouldClose() menjadi syarat perulangan dan mengembalikan true ketika ESC ditekan atau ikon tutup jendela diklik.
  • rl.IsKeyDown membaca tombol yang ditahan, sedangkan rl.IsKeyPressed membaca penekanan satu kali, dengan argumen anggota enum KeyboardKey seperti .RIGHT dan .SPACE.

Referensi#

  • Overview — Odin Documentation — Dokumentasi resmi Odin yang menjelaskan pola import pustaka vendor beserta aliasnya.
  • raylib — GitHub — Repositori pustaka C yang dibundel sebagai vendor:raylib, memuat cheatsheet proc jendela dan input.
  • Examples — raylib.com — Kumpulan contoh resmi raylib yang memuat contoh jendela dasar dan pembacaan input keyboard.