Membuat AI Game Hoki Menggunakan Steering Behaviors: Game Mechanics
() translation by (you can also view the original English article)
Dalam posting terakhir di seri ini, kami telah berfokus pada konsep di
balik kecerdasan buatan yang telah kami pelajari. Pada bagian ini, kita akan
menyelesaikan semua implementasi menjadi game hoki yang dapat dimainkan. Anda akan belajar menambahkan potongan yang hilang yang diperlukan untuk mengubahnya menjadi sebuah game, seperti skor, power-up, dan sedikit desain game.
Hasil final
Berikut adalah game yang akan diimplementasikan dengan menggunakan semua elemen yang akan dijelaskan dalam tutorial ini.
Berpikir Dengan Disain Permainan
Bagian sebelumnya dari seri ini difokuskan untuk menjelaskan bagaimana permainan AI bekerja. Setiap bagian menjelaskan secara detail tentang aspek tertentu dari sebuah game, seperti bagaimana atlet bergerak dan bagaimana serangan dan pertahanan diterapkan. Mereka didasarkan pada konsep seperti steering behaviors dan stack-based finite state machines.
Untuk membuat permainan yang dapat dimainkan sepenuhnya, bagaimanapun, semua aspek itu harus dibungkus menjadi inti sebuah gamemekanik. Pilihan yang paling jelas adalah menerapkan semua peraturan resmi pertandingan hoki yang sesungguhnya, tapi ini akan membutuhkan banyak kerja dan waktu. Mari kita mengambil pendekatan fantasi yang lebih sederhana.
Semua peraturan hoki akan diganti dengan yang tunggal: jika Anda membawa keping dan disentuh oleh lawan, maka Anda telah membekukan dan menghancurkan jutaan keping! Ini akan membuat permainan lebih mudah dimainkan dan menyenangkan bagi kedua pemain: yang membawa keping dan yang mencoba memulihkannya.
Untuk meningkatkan mekanik ini, kami akan menambahkan beberapa power-up. Mereka akan membantu pemain mencetak gol dan membuat permainan sedikit lebih dinamis.
Menambahkan Kemampuan untuk Skor
Mari kita mulai dengan sistem penilaian, yang bertanggung jawab untuk menentukan siapa yang menang atau kalah. Tim akan dinilai setiap kali puck (bola hoki) memasuki gawang lawan.
Cara termudah untuk menerapkannya adalah dengan menggunakan dua persegi panjang yang tumpang tindih:



Persegi panjang hijau mewakili daerah yang ditempati oleh struktur gol (bingkai dan jaring). Ia bekerja seperti blok padat, sehingga puck dan atlit tidak akan bisa bergerak melewatinya; mereka akan terhempas kembali
Kotak merah mewakili "area skor". Jika puck menumpang tindih persegi panjang ini, itu berarti tim baru
saja mencetak gol.
Kotak merah berukuran lebih kecil dari yang hijau, dan diletakkan di depannya, jadi jika puck menyentuh gawang di sisi manapun tapi di depan, bola akan kembali dan tidak ada skor yang akan ditambahkan:



Mengorganisir Segalanya Sesudah Seseorang
Setelah tim di beri skor, semua atlet harus kembali ke posisi awal dan puck harus ditempatkan di pusat arena lagi. Setelah proses ini, pertandingan bisa dilanjutkan.
Memindahkan Atlet ke Posisi Awal Mereka
Seperti
yang dijelaskan di bagian pertama seri ini, semua atlet memiliki kedudukan AI yang
disebut prepareForMatch
yang akan memindahkan mereka ke posisi awal, dan
menyebabkannya berhenti secara perlahan di tempat tersebut.
Ketika puck tumpang tindih dengan
salah satu "area skor", kedudukan aktif AI para atlet saat ini akan
dibatalkan dan prepareForMatch
akan otomatis terbentuk. Di mana pun para atlet
berada, mereka akan kembali ke posisi awal mereka setelah beberapa detik:
Memindahkan Kamera Menuju Pusat Rink
Karena kamera selalu mengikuti kemanapun puck bergerak, maka jika kamera langsung berputar ke pusat arena setelah seseorang mendapat nilai, pandangan saat ini akan tiba-tiba berubah, pemandangan yang akan tertangkap pada saat itu menjadi jelek dan membingungkan.
Cara yang lebih baik untuk melakukan ini adalah dengan memindahkan puck dengan lancar menuju pusat arena; Karena kamera mengikuti puck-nya, sehingga kamera dapat dengan anggun akan meluncur dari gawang ke pusat arena.
Hal ini dapat dicapai dengan mengubah vektor kecepatan puck setelah menyentuh area sasaran. Varian kecepatan baru harus "mendorong" puck ke arah pusat arena, sehingga dapat dihitung sebagai:
1 |
var c :Vector3D = getRinkCenter(); |
2 |
var p :Vector3D = puck.position; |
3 |
|
4 |
var v :Vector3D = c - p; |
5 |
v = normalize(v) * 100; |
6 |
|
7 |
puck.velocity = v; |
Dengan mengurangi posisi pusat arena dari posisi puck saat ini, sangatlah meungkinkan untuk menghitung vektor yang mengarah langsung ke pusat arena.
Setelah
menormalisasi vektor ini, ini dapat diskalakan dengan nilai berapapun, misalnya
100
, yang mengontrol seberapa cepat puck
bergerak menuju pusat arena.
Berikut adalah gambar dengan representasi vektor kecepatan baru:



Vektor V
ini digunakan sebagai vektor kecepatan puck, sehingga puck-nya akan bergerak menuju pusat arena seperti yang diinginkan.
Untuk mencegah perilaku asing sketika puck bergerak menuju pusat arena, seperti interaksi dengan atlet, maka puck akan dinonaktifkan selama proses berlangsung. Sebagai konsekuensinya, ia berhenti berinteraksi dengan atlit dan ditandai sebagai tidak terlihat. Pemain tidak akan melihat puck-nya bergerak, tapi kamera tetap akan mengikutinya.
Untuk menentukan apakah puck sudah berada
dalam posisinya, jarak antara posisi tersebut dan pusat arena dihitung selama
gerakan. Jika kurang dari 10
, misalnya, puck
cukup dekat untuk ditempatkan langsung di pusat arena dan diaktifkan kembali
sehingga pertandingan bisa berlanjut.
Menambahkan Level Kekuatan (Power-Up)
Gagasan di balik menambah level kekuatan adalah membantu pemain mencapai tujuan utama permainan, yaitu mencetak gol dengan membawa puck ke gawang lawan.
Dikarenakan jangkauan, permainan kita hanya akan memiliki dua power-up: Ghost Help and Fear The Puck. Pada awalnya kita menambahkan tiga atlet tambahan ke tim pemain untuk beberapa waktu, namun untuk selanjutnya kita membuat lawan lari dari puck selama beberapa detik.
Power-up ditambahkan ke kedua tim saat ada yang mendapat nilai.
Menerapkan Power-up "Ghost Help"
Karena semua atlit yang ditambahkan oleh power-up Ghost Help bersifat
sementara, kelas Athlete
harus dimodifikasi untuk memungkinkan atlit ditandai
sebagai "ghost". Jika atlit adalah ghost, ia akan melepaskan diri dari permainan setelah beberapa
detik.
Di bawah ini adalah kelas Athlete
, sorotilah penambahan yang dibuat untuk
mengakomodasi fungsi ghost:
1 |
public class Athlete |
2 |
{
|
3 |
// (...)
|
4 |
private var mGhost :Boolean; // tells if the athlete is a ghost (a powerup that adds new athletes to help steal the puck). |
5 |
private var mGhostCounter :Number; // counts the time a ghost will remain active |
6 |
|
7 |
public function Athlete(thePosX :Number, thePosY :Number, theTotalMass :Number) { |
8 |
// (...)
|
9 |
mGhost = false; |
10 |
mGhostCounter = 0; |
11 |
|
12 |
// (...)
|
13 |
}
|
14 |
|
15 |
public function setGhost(theStatus :Boolean, theDuration :Number) :void { |
16 |
mGhost = theStatus; |
17 |
mGhostCounter = theDuration; |
18 |
}
|
19 |
|
20 |
public function amIAGhost() :Boolean { |
21 |
return mGhost; |
22 |
}
|
23 |
|
24 |
public function update() :void { |
25 |
// (...)
|
26 |
|
27 |
// Update powerup counters and stuff
|
28 |
updatePowerups(); |
29 |
|
30 |
// (...)
|
31 |
}
|
32 |
|
33 |
public function updatePowerups() :void { |
34 |
// TODO.
|
35 |
}
|
36 |
}
|
Properti
mGhost
adalah sebuah boolean yang memberitahu jika atlit adalah ghost atau tidak, sementara
mGhostCounter
berisi jumlah detik yang harus ditunggu atlet sebelum melepaskan
dirinya dari permainan.
Kedua properti tersebut digunakan oleh metode updatePowerups
():
1 |
private function updatePowerups():void { |
2 |
// If the athlete is a ghost, it has a counter that controls
|
3 |
// when it must be removed.
|
4 |
if (amIAGhost()) { |
5 |
mGhostCounter -= time_elapsed; |
6 |
|
7 |
if (mGhostCounter <= 2) { |
8 |
// Make athlete flicker when it is about to be removed.
|
9 |
flicker(0.5); |
10 |
}
|
11 |
|
12 |
if (mGhostCounter <= 0) { |
13 |
// Time to leave this world! (again)
|
14 |
kill(); |
15 |
}
|
16 |
}
|
17 |
}
|
Metode updatePowerups ()
,
yang disebut dalam rutinitas pemutakhiran atlet (),
akan menangani semua
pemrosesan power-up di atlet. Sekarang semua yang dilakukannya adalah memeriksa apakah atlit saat ini adalah ghost atau tidak. Jika iya, maka properti mGhostCounter
dikurangi dengan jumlah waktu yang telah berlalu sejak update terakhir.
Bila
nilai mGhostCounter
mencapai nol, berarti atlet
temporer tersebut sudah cukup lama aktif, sehingga harus keluar dari permainan.
Untuk membuat pemain sadar akan hal tersebut, atlet akan mulai berkedip dua
detik terakhir sebelum menghilang.
Akhirnya, saatnya untuk menerapkan proses penambahan atlet temporer saat
power-up diaktifkan. Ini dilakukan dalam metode powerupGhostHelp
(),
tersedia dalam logika game utama:
1 |
private function powerupGhostHelp() :void { |
2 |
var aAthlete :Athlete; |
3 |
|
4 |
for (var i:int = 0; i < 3; i++) { |
5 |
// Add the new athlete to the list of athletes
|
6 |
aAthlete = addAthlete(RINK_WIDTH / 2, RINK_HEIGHT - 100); |
7 |
|
8 |
// Mark the athlete as a ghost which will be removed after 10 seconds.
|
9 |
aAthlete.setGhost(true, 10); |
10 |
}
|
11 |
}
|
Metode ini dilakukan berulangkali lebih dari satu putaran yang sesuai dengan jumlah atlet temporer yang ditambahkan. Setiap atlit baru ditambahkan ke bawah arena dan ditandai sebagai ghost.
Seperti yang dijelaskan sebelumnya, atlet hantu akan melepaskan diri dari permainan.
Menerapkan Power-Up "Fear The Puck"
Power-up Fear The Puck membuat semua lawan melarikan diri dari puck selama beberapa detik.
Sama seperti power-up Ghost Help,
kelas Athlete
harus dimodifikasi untuk mengakomodasi fungsi itu:
1 |
public class Athlete |
2 |
{
|
3 |
// (...)
|
4 |
private var mFearCounter :Number; // counts the time the athlete should evade from puck (when fear powerup is active). |
5 |
|
6 |
|
7 |
public function Athlete(thePosX :Number, thePosY :Number, theTotalMass :Number) { |
8 |
// (...)
|
9 |
mFearCounter = 0; |
10 |
|
11 |
// (...)
|
12 |
}
|
13 |
|
14 |
public function fearPuck(theDuration: Number = 2) :void { |
15 |
mFearCounter = theDuration; |
16 |
}
|
17 |
|
18 |
// Returns true if the mFearCounter has a value and the athlete
|
19 |
// is not idle or preparing for a match.
|
20 |
private function shouldIEvadeFromPuck() :Boolean { |
21 |
return mFearCounter > 0 && mBrain.getCurrentState() != idle && mBrain.getCurrentState() != prepareForMatch; |
22 |
}
|
23 |
|
24 |
private function updatePowerups():void { |
25 |
if(mFearCounter > 0) { |
26 |
mFearCounter -= elapsed_time; |
27 |
}
|
28 |
|
29 |
// (...)
|
30 |
}
|
31 |
|
32 |
public function update() :void { |
33 |
// (...)
|
34 |
|
35 |
// Update powerup counters and stuff
|
36 |
updatePowerups(); |
37 |
|
38 |
// If the athlete is an AI-controlled opponent
|
39 |
if (amIAnAiControlledOpponent()) { |
40 |
// Check if "fear of the puck" power-up is active.
|
41 |
// If that's true, evade from puck.
|
42 |
if(shouldIEvadeFromPuck()) { |
43 |
evadeFromPuck(); |
44 |
}
|
45 |
}
|
46 |
|
47 |
// (...)
|
48 |
}
|
49 |
|
50 |
public function evadeFromPuck() :void { |
51 |
// TODO
|
52 |
}
|
53 |
}
|
Pertama,
metode updatePowerups ()
diubah menjadi penurunan properti mFearCounter
, yang
berisi jumlah waktu atlet harus untuk menghindar dari puck-nya. Properti mFearCounter
diubah setiap kali metode fearPuck ()
dipanggil.
Dalam metode update Athlete (),
sebuah tes ditambahkan untuk memeriksa apakah power-up harus dilakukan. Jika
atlit adalah lawan yang dikendalikan oleh AI (amIAnAiControlledOpponent()
returns true
) dan atlet harus menghindari puck (shouldievadeFromPuck()
juga mengembalikan nilai true
),
metode evadeFromPuck ()
akan dimunculkan.
Metode evadeFromPuck ()
menggunakan evade behavior, yang membuat entitas menghindari objek dan lintasannya:
1 |
private function evadeFromPuck() :void { |
2 |
mBoid.steering = mBoid.steering + mBoid.evade(getPuck().getBoid()); |
3 |
}
|
Semua metode evadeFromPuck
()
menambahkan kekuatan penghindar ke kekuatan kemudi atlet
saat ini. Ini membuat dia menghindari puck tanpa mengabaikan kekuatan kemudi
yang sudah ditambahkan, seperti yang diciptakan oleh negara AI yang aktif saat
ini.
Agar bisa dihindari, keping harus berperilaku seperti boid, seperti yang semua atlet lakukan (informasi lebih lengkap
ada di bagian pertama seri ini). Sebagai konsekuensinya, properti boid, yang
berisi posisi dan kecepatan puck saat ini, harus ditambahkan ke kelas Puck
:
1 |
class Puck { |
2 |
// (...)
|
3 |
private var mBoid :Boid; |
4 |
|
5 |
// (...)
|
6 |
|
7 |
public function update() { |
8 |
// (...)
|
9 |
mBoid.update(); |
10 |
}
|
11 |
|
12 |
public function getBoid() :Boid { |
13 |
return mBoid; |
14 |
}
|
15 |
|
16 |
// (...)
|
17 |
}
|
Akhirnya, kami memperbarui logika permainan utama untuk membuat lawan takut akan puck saat power-up diaktifkan:
1 |
private function powerupFearPuck() :void { |
2 |
var i :uint, |
3 |
athletes :Array = rightTeam.members, |
4 |
size :uint = athletes.length; |
5 |
|
6 |
for (i = 0; i < size; i++) { |
7 |
if (athletes[i] != null) { |
8 |
// Make athlete fear the puck for 3 seconds.
|
9 |
athletes[i].fearPuck(3); |
10 |
}
|
11 |
}
|
12 |
}
|
Metode ini dilakukan berulang atas semua atlet
lawan (tim kanan, dalam kasus ini), memanggil metode fearkPuck()
pada
setiap dari mereka. Hal ini akan memicu logika yang membuat atlet menjauhi puck dalam beberapa detik, seperti yang
dijelaskan sebelumnya.
Pembekuan dan penghancuran
Penambahan permainan pada permainan adalah bagian pembekuan dan penghancuran. Hal itu dilakukan dalam logika permainan utama, di mana pemeriksaan rutin apakah atlet tim kiri tumpang tindih dengan atlit tim kanan.
Pemeriksaan tumpang tindih ini secara otomatis dilakukan oleh mesin permainan Flixel, yang memanggil setiap kali tumpang tindih ditemukan:
1 |
private function athletesOverlapped(theLeftAthlete :Athlete, theRightAthlete :Athlete) :void { |
2 |
// Does the puck have an owner?
|
3 |
if (mPuck.owner != null) { |
4 |
// Yes, it does.
|
5 |
if (mPuck.owner == theLeftAthlete) { |
6 |
//Puck's owner is the left athlete
|
7 |
theLeftAthlete.shatter(); |
8 |
mPuck.setOwner(theRightAthlete); |
9 |
|
10 |
} else if (mPuck.owner == theRightAthlete) { |
11 |
//Puck's owner is the right athlete
|
12 |
theRightAthlete.shatter(); |
13 |
mPuck.setOwner(theLeftAthlete); |
14 |
}
|
15 |
}
|
16 |
}
|
Pemanggilan ini diterima sebagai parameter para atlet dari masing-masing tim yang tumpang tindih. Sebuah tes yang dilakukan umtuk memerikas apakah pemilik puck itu tidak dibatalkan, yang berarti sedang dibawa oleh seseorang.
Dalam hal ini, pemilik puck itu dibandingkan dengan atlit yang tumpang tindih. Jika salah satu dari mereka membawa puck (jadi dia adalah pemilik puck), maka atlit tersebut hancur dan kepemilikan puck beralih pada atlet lainnya.
Metode shatter()
di
kelas Athlete
akan menandai ketidak aktifan
atlit dan menempatkannya di dasar arena setelah beberapa detik. Ini juga akan
memancarkan beberapa partikel yang merepresentasikan potongan es, tapi topik
ini akan dibahas di artikel lain.
Kesimpulan
Dalam tutorial ini, kami menerapkan beberapa elemen yang diperlukan untuk mengubah prototipe hoki menjadi permainan yang dapat dimainkan sepenuhnya. Saya sengaja menempatkan fokus pada konsep di balik masing-masing elemen tersebut, daripada bagaiman sebenarnya menerapkannya di mesin game X atau Y.
Pendekatan pembekuan dan penghancuran yang digunakan untuk game mungkin terdengar terlalu fantastis, namun membantu agar proyek tetap dikelola. Aturan olahraga sangat spesifik, dan implementasinya bisa rumit.
Dengan menambahkan beberapa layar dan beberapa elemen HUD, Anda dapat membuat permainan hoki Anda sendiri dari demo ini!
Referensi
- Rink: Hockey Stadium on GraphicRiver
- Sprites: Hockey Players by Taylor J Glidden
- Icons: Game-Icons by Lorc
- Mouse cursor: Cursor by Iwan Gabovitch
- Instruction keys: Keyboard Pack by Nicolae Berbece
- Crosshair: Crosshairs Pack by Bryan
- SFX/Music: shatter by Michel Baradari, puck hit and cheer by gr8sfx, music by DanoSongs.com