Matematika untuk game

Vektor, matriks, dan transformasi dari core:math.

Daftar isi

raylib mengurus jendela dan penggambaran, sedangkan gerak objek, pemeriksaan jarak, dan penempatan titik ditentukan oleh hitungan matematika yang ditulis dalam kode game. Topik ini membahas perangkat matematika dari koleksi core untuk keperluan tersebut: tipe vektor yang juga dipakai seluruh proc raylib, operasi vektor dua dimensi seperti panjang, normalisasi, dot product, dan jarak antartitik, serta matriks translasi, rotasi, dan skala beserta penerapannya pada sebuah titik. Seluruh contoh berupa hitungan murni tanpa jendela, sehingga dapat dijalankan di lingkungan mana pun, termasuk lingkungan tanpa tampilan (headless).

Tipe Vector2 dari raylib dan core:math#

Tipe vektor yang dipakai raylib untuk koordinat dua dimensi adalah rl.Vector2, yang pada berkas binding vendor/raylib/raylib.odin didefinisikan sebagai Vector2 :: [2]f32. Paket core:math/linalg mendefinisikan bentuk yang sama sebagai Vector2f32 :: [2]f32. Keduanya bukan dua tipe terpisah yang kebetulan mirip, melainkan alias untuk tipe array yang persis sama, sehingga nilai yang dihitung dengan proc core dapat diteruskan ke proc menggambar raylib tanpa konversi. Program contoh topik ini meneruskan nilai rl.Vector2 hasil hitungan core:math/linalg langsung ke proc rl.Vector2Transform, dan pola yang sama berlaku untuk Vector3 serta Vector4.

Odin menyediakan akses field .x dan .y untuk array tetap [2]f32 tanpa deklarasi tambahan, sehingga posisi.x dan posisi.y terbaca seperti anggota struct. Penjumlahan, pengurangan, dan perkalian dengan skalar juga berlaku langsung pada tipe ini, misalnya target - posisi menghasilkan vektor arah dari satu titik ke titik lain. Kombinasi akses field dan aritmetika array membuat operasi gerak sederhana dapat ditulis tanpa memanggil proc khusus.

Operasi vektor dua dimensi#

Panjang (magnitude) vektor v=(x,y)v = (x, y) dihitung dengan teorema Pythagoras:

∥v∥=x2+y2 \lVert v \rVert = \sqrt{x^2 + y^2}

Paket core:math/linalg menyediakan perhitungan tersebut sebagai linalg.length, ditambah linalg.length2 yang mengembalikan kuadratnya tanpa akar untuk perbandingan panjang yang lebih murah. Vektor satuan (unit vector), yaitu vektor berpanjang satu yang hanya menyimpan arah, diperoleh dari linalg.normalize yang membagi setiap komponen dengan panjang vektornya.

Perkalian titik (dot product) dinyatakan a⋅b=axbx+aybya \cdot b = a_x b_x + a_y b_y dan dihitung dengan linalg.dot. Nilainya positif bila kedua vektor searah, nol bila tegak lurus, dan negatif bila berlawanan arah, sehingga cocok untuk menguji apakah sebuah titik berada di depan atau di belakang arah gerak. Jarak antara dua titik tersedia sebagai linalg.distance(a, b), yang menghitung panjang dari selisih kedua titiknya.

Proc lain yang dibutuhkan ditulis sendiri di atas proc core, karena aritmetika vektornya sudah berlaku pada tipe array. Jarak antartitik, misalnya, dapat dinyatakan kembali sebagai linalg.length(b - a) pada proc sendiri apabila pembacaan kode lebih diutamakan. Paket core menyediakan bentuk yang paling sering dipakai, sedangkan kombinasi khusus milik game ditulis di atasnya.

Contoh: drone pemantau area#

Program berikut merangkai operasi sebelumnya pada kasus drone pemantau area. Drone berada di posisi (40, 25) dan harus bergerak ke titik pemeriksaan (40, 105) dengan laju 20 satuan per detik. Arah menuju target dinormalisasi lalu dikalikan laju menjadi vektor kecepatan, dua titik amatan diuji posisinya terhadap arah gerak dengan dot product, dan bagian akhir program menyiapkan matriks model yang dibahas pada bagian berikutnya. Arah hadap drone dihitung dengan math.atan2, sehingga sudutnya ikut tercetak tanpa perhitungan terpisah.

odin main.odin
package main

import "core:fmt"
import "core:math"
import "core:math/linalg"
import rl "vendor:raylib"

// jarak antar titik dinyatakan sebagai panjang vektor selisihnya
jarak :: proc(a, b: rl.Vector2) -> f32 {
	return linalg.length(b - a)
}

main :: proc() {
	posisi := rl.Vector2{40, 25}
	target := rl.Vector2{40, 105}
	laju: f32 = 20

	// arah menuju target dinormalisasi, lalu dikalikan laju menjadi kecepatan
	arah := target - posisi
	satuan := linalg.normalize(arah)
	kecepatan := satuan * laju

	fmt.println("posisi       =", posisi)
	fmt.println("target       =", target)
	fmt.println("jarak sisa   =", jarak(posisi, target))
	fmt.println("arah         =", arah)
	fmt.println("arah satuan  =", satuan)
	fmt.println("kecepatan    =", kecepatan)

	// dot product menguji posisi dua titik amatan terhadap arah gerak
	depan    := linalg.normalize(rl.Vector2{40, 120} - posisi)
	belakang := linalg.normalize(rl.Vector2{40, 10} - posisi)
	fmt.println("dot depan    =", linalg.dot(satuan, depan))
	fmt.println("dot belakang =", linalg.dot(satuan, belakang))

	// matriks model: skala, rotasi sesuai arah hadap, lalu translasi
	sudut := math.atan2(satuan.y, satuan.x)
	model := rl.MatrixTranslate(posisi.x, posisi.y, 0) *
		rl.MatrixRotateZ(sudut) *
		rl.MatrixScale(2, 2, 1)

	// titik lokal ujung sensor (0, 2) diangkat ke koordinat dunia
	sensor := rl.Vector2Transform({0, 2}, model)
	fmt.println("arah hadap (derajat) =", sudut * math.DEG_PER_RAD)
	fmt.println("sensor di dunia      =", sensor)
}
bash
# jalankan program matematika dari direktori topik
odin run main.odin -file
text Keluaran
posisi       = [40, 25]
target       = [40, 105]
jarak sisa   = 80
arah         = [0, 80]
arah satuan  = [0, 1]
kecepatan    = [0, 20]
dot depan    = 1
dot belakang = -1
arah hadap (derajat) = 90
sensor di dunia      = [36, 25]

Vektor arah (0, 80) dinormalisasi menjadi (0, 1), sehingga kecepatannya (0, 20) dan drone bergerak sejajar sumbu y dengan laju yang tetap. Titik amatan pertama berada searah arah gerak, sehingga dot product-nya 1, sedangkan amatan kedua berlawanan arah dengan nilai -1. Nilai jarak sisa 80 dibaca langsung dari panjang vektor arah, karena proc jarak dan linalg.length(arah) menghitung besaran yang sama. Tipe rl.Vector2 dipakai langsung sebagai tipe parameter proc jarak, dan nilai-nilai yang sama dapat diberikan ke proc menggambar raylib tanpa konversi tipe.

Matriks dan transformasi titik#

raylib mewakili transformasi dengan rl.Matrix, yang didefinisikan sebagai #row_major matrix[4, 4]f32. Matriks empat kali empat tetap dipakai pada konteks dua dimensi karena translasi menempati kolom keempat dan komposisi antarmatriks menuntut ukuran yang sama. Proc pembuatnya berada di berkas binding vendor/raylib/raymath.odin: rl.MatrixTranslate(x, y, z) menggeser titik, rl.MatrixRotateZ(angle) memutar pada bidang layar dengan sudut dalam radian, dan rl.MatrixScale(x, y, z) mengubah ukuran.

Beberapa transformasi digabung dengan operator * menurut bentuk p′=T⋅R⋅S⋅pp' = T \cdot R \cdot S \cdot p, dan matriks yang paling kanan diterapkan pada titik lebih dahulu. Pada program contoh, titik lokal ujung sensor (0, 2) mengalami skala 2 menjadi (0, 4), kemudian rotasi 90 derajat memutarnya menjadi (-4, 0), dan translasi terakhir menggesernya ke (36, 25). Proc lama seperti MatrixIdentity dan MatrixMultiply ditandai deprecated oleh binding dan digantikan dengan Matrix(1) serta operator *.

Titik ditransformasikan dengan matriks melalui rl.Vector2Transform(v, m), yang mengangkat vektor dua dimensi ke koordinat homogen (homogeneous coordinates) sebagai (x, y, 0, 1), mengalikannya dengan matriks, lalu mengambil kembali komponen x dan y. Arah hadap drone dihitung dengan math.atan2(satuan.y, satuan.x), yakni cara baku untuk memperoleh sudut dari vektor arah. Sudut positif menghasilkan putaran berlawanan arah jarum jam pada bidang matematika, dan karena sumbu y layar raylib mengarah ke bawah, putaran yang sama tampak searah jarum jam saat digambar.

Kesalahan umum#

Kesalahan pertama adalah menormalisasi vektor nol. linalg.normalize membagi setiap komponen dengan panjang vektor, sehingga vektor berpanjang nol menghasilkan [NaN, NaN] yang terbawa ke seluruh hitungan posisi berikutnya. Gunakan linalg.normalize0, yang mengembalikan vektor nol apabila panjangnya nol, atau periksa linalg.length sebelum menormalisasi. Proc rl.Vector2Normalize dari raymath sudah memakai bentuk aman yang sama.

Kesalahan kedua adalah mengirim sudut dalam derajat ke proc yang menerima radian. rl.MatrixRotateZ, math.cos, dan math.sin bekerja dalam radian, sehingga sudut 90 derajat harus dikonversi dengan math.RAD_PER_DEG terlebih dahulu. Arah konversinya dibalik saat sudut radian ditampilkan, yaitu dengan math.DEG_PER_RAD sebagaimana dipakai program contoh untuk mencetak 90 pada arah hadap drone.

Kesalahan ketiga adalah menukar urutan faktor komposisi. Matriks t * r * s dan s * r * t menghasilkan matriks yang berbeda karena matriks paling kanan diterapkan lebih dahulu. Skala yang diterapkan setelah rotasi mengubah bentuk hasil, dan translasi yang diputar ikut berpindah. Ketika hasil transformasi tampak salah, periksa urutan faktornya sebelum mengubah nilai sudut atau skala.

Ringkasan#

  • rl.Vector2 adalah alias [2]f32, tipe yang sama dengan Vector2f32 dari core:math/linalg, sehingga nilai berpindah antara hitungan core dan proc raylib tanpa konversi.
  • linalg.length, linalg.normalize, linalg.dot, dan linalg.distance menutupi operasi vektor dua dimensi yang paling sering dipakai, dan proc tambahan ditulis di atasnya.
  • Matriks rl.Matrix dibangun dari rl.MatrixTranslate, rl.MatrixRotateZ, dan rl.MatrixScale, digabung dengan * ketika matriks paling kanan diterapkan lebih dahulu, lalu diterapkan pada titik melalui rl.Vector2Transform.
  • Normalisasi vektor nol dan derajat yang dikirim ke proc yang menerima radian adalah dua sumber NaN dan hasil transformasi yang keliru.

Referensi#