Membuat Game Hoki AI dengan Menggunakan Steering Behaviors: Pondasi
() translation by (you can also view the original English article)
Ada berbagai cara untuk membuat game tertentu. Biasanya, seorang pengembang memilih sesuatu yang sesuai dengan keahliannya, menggunakan teknik yang telah dia ketahui untuk menghasilkan hasil terbaik. Terkadang, orang belum tahu bahwa mereka memerlukan teknik tertentu – bahkan mungkin lebih mudah dan lebih baik - hanya karena mereka sudah tahu cara untuk menciptakan game itu.
Dalam rangkaian tutorial ini, Anda akan belajar bagaimana menciptakan kecerdasan buatan untuk permainan hoki menggunakan kombinasi teknik, seperti steering behaviors, yang sebelumnya saya jelaskan sebagai sebuah konsep.
Catatan: Meskipun tutorial ini ditulis menggunakan AS3 dan Flash, Anda harus bisa menggunakan teknik dan konsep yang sama di hampir semua lingkungan pengembangan game.
Perkenalan
Hoki adalah olahraga yang menyenangkan dan populer, dan seperti video game, game ini menggabungkan banyak topik gamedev, seperti pola pergerakan, kerja tim (serangan, pertahanan), kecerdasan buatan, dan taktik. Permainan hoki yang bisa dimainkan sangat sesuai untuk menunjukkan kombinasi beberapa teknik yang berguna.
Untuk mensimulasikan mekanik hoki, dengan atlet yang berlari dan bergerak, merupakan tantangan tersendiri. Jika pola pergerakan sudah ditentukan sebelumnya, meski dengan jalur yang berbeda, permainan menjadi mudah ditebak (dan membosankan). Bagaimana kita bisa menerapkan lingkungan dinamis seperti itu sambil tetap mengendalikan apa yang sedang terjadi? Jawabannya adalah: menggunakan steering behaviors.
Steering behaviors bertujuan untuk menciptakan pola gerakan yang realistis dengan navigasi improvisasi. Mereka didasarkan pada kekuatan sederhana yang digabungkan setiap update game, jadi sangat dinamis. Hal ini menjadikan mereka pilihan sempurna untuk menerapkan sesuatu yang kompleks dan dinamis seperti hoki atau permainan sepak bola.
Melamar Kerja
Demi waktu dan pengajaran, mari kita mengurangi ruang lingkup permainan sedikit. Permainan hoki kami hanya akan mengikuti satu set kecil aturan asli olahraga: dalam permainan kami tidak akan ada adu penalti dan tidak ada penjaga gawang, jadi setiap atlet bisa bergerak di sekitar arena:



Setiap gawang akan digantikan oleh "tembok" kecil tanpa jaring. Agar bisa mencetak gol, tim harus menggerakkan puck (bola hoki) untuk membuatnya menyentuh sisi manapun dari sasaran lawan. Saat seseorang mendapat nilai, kedua tim akan mengaturnya kembali, dan pucknya akan ditempatkan di tengah; Pertandingan akan dimulai ulang beberapa detik setelah itu.
Mengenai penanganan puck: jika seorang atlit, misalnya atlit A memiliki puck dan disentuh oleh lawan, katakanlah atlit B, lalu B meraih puck dan A menjadi tidak bergerak selama beberapa detik. Jika puck itu meninggalkan arena, maka akan segera ditempatkan di pusat arena.
Saya akan menggunakan mesin permainan Flixel untuk mengurus bagian grafis dari kode. Namun, kode mesin akan disederhanakan atau dihilangkan dalam contoh, untuk tetap fokus pada game itu sendiri.
Penataan Lingkungan
Mari kita mulai dengan lingkungan permainan, yang terdiri dari arena, sejumlah atlet, dan dua gawang. Arena pertandingan terbuat dari empat persegi panjang yang ditempatkan di sekitar area es; persegi panjang ini akan bertabrakan dengan segala sesuatu yang menyentuh mereka, jadi tidak ada yang akan meninggalkan daerah es.
Seorang atlet akan digambarkan oleh kelas Athlete
:
1 |
public class Athlete |
2 |
{
|
3 |
private var mBoid :Boid; // controls the steering behavior stuff |
4 |
private var mId :int; // a unique identifier for the athelete |
5 |
|
6 |
public function Athlete(thePosX :Number, thePosY :Number, theTotalMass :Number) { |
7 |
mBoid = new Boid(thePosX, thePosY, theTotalMass); |
8 |
}
|
9 |
|
10 |
public function update():void { |
11 |
// Clear all steering forces
|
12 |
mBoid.steering = null; |
13 |
|
14 |
// Wander around
|
15 |
wanderInTheRink(); |
16 |
|
17 |
// Update all steering stuff
|
18 |
mBoid.update(); |
19 |
}
|
20 |
|
21 |
private function wanderInTheRink() :void { |
22 |
var aRinkCenter :Vector3D = getRinkCenter(); |
23 |
|
24 |
// If the distance from the center is greater than 80,
|
25 |
// move back to the center, otherwise keep wandering.
|
26 |
if (Utils.distance(this, aRinkCenter) >= 80) { |
27 |
mBoid.steering = mBoid.steering + mBoid.seek(aRinkCenter); |
28 |
} else { |
29 |
mBoid.steering = mBoid.steering + mBoid.wander(); |
30 |
}
|
31 |
}
|
32 |
}
|
Properti mBoid
adalah turunan dari kelas Boid
, sebuah enkapsulasi logika matematika yang digunakan dalam rangkaian steering behaviors. Contoh mBoid
antara lain, vektor matematika yang menggambarkan arah saat ini, kekuatan kemudi, dan posisi entitas.
Metode update()
di kelas Athlete
akan digunakan setiap kali update game. Untuk saat ini, metode tersebut hanya digunakan untuk membersihkan kekuatan kemudi yang aktif, menambahkan gaya wander, dan akhirnya memanggil mBoid.update()
. Perintah sebelumnya memperbarui semua logika perilaku kemudi yang dienkapsulasi dalam mBoid
, sehingga membuat atlit bergerak (menggunakan Euler integration).
Kelas game yang bertanggung jawab untuk game loop, akan disebut PlayState
. Kelas game ini memiliki arena, dua kelompok atlet (satu kelompok untuk masing-masing tim) dan dua gol:
1 |
public class PlayState |
2 |
{
|
3 |
private var mAthletes :FlxGroup; |
4 |
private var mRightGoal :Goal; |
5 |
private var mLeftGoal :Goal; |
6 |
|
7 |
public function create():void { |
8 |
// Here everything is created and added to the screen.
|
9 |
}
|
10 |
|
11 |
override public function update():void { |
12 |
// Make the rink collide with athletes
|
13 |
collide(mRink, mAthletes); |
14 |
|
15 |
// Ensure all athletes will remain inside the rink.
|
16 |
applyRinkContraints(); |
17 |
}
|
18 |
|
19 |
private function applyRinkContraints() :void { |
20 |
// check if athletes are within the rink
|
21 |
// boundaries.
|
22 |
}
|
23 |
}
|
Dengan asumsi bahwa satu atlet ditambahkan ke pertandingan, sejauh ini berikut adalah hasil dari semuanya:
Mengikuti Kursor Mouse
Atlet harus mengikuti kursor mouse, sehingga pemain benar-benar bisa mengendalikan sesuatu. Karena kursor mouse memiliki posisi di layar, maka bisa dijadikan tujuan untuk arrival behavior.
Arrival behavior akan membuat seorang atlet mencari posisi kursor, memperlambat kecepatan dengan cepat saat mendekati kursor, dan akhirnya berhenti di situ.
Di kelas Athlete
, mari kita ganti metode wandering dengan arrival behavior:
1 |
public class Athlete |
2 |
{
|
3 |
// (...)
|
4 |
|
5 |
public function update():void { |
6 |
// Clear all steering forces
|
7 |
mBoid.steering = null; |
8 |
|
9 |
// The athlete is controlled by the player,
|
10 |
// so just follow the mouse cursor.
|
11 |
followMouseCursor(); |
12 |
|
13 |
// Update all steering stuff
|
14 |
mBoid.update(); |
15 |
}
|
16 |
|
17 |
private function followMouseCursor() :void { |
18 |
var aMouse :Vector3D = getMouseCursorPosition(); |
19 |
mBoid.steering = mBoid.steering + mBoid.arrive(aMouse, 50); |
20 |
}
|
21 |
}
|
Hasilnya adalah atlet yang bisa kursor mouse. Karena logika gerakan didasarkan pada perilaku kemudi, para atlet menavigasi arena dengan cara yang meyakinkan dan mulus.
Gunakan kursor mouse untuk membimbing atlet dalam demo di bawah ini:
Menambah dan Mengontrol Puck
Puck akan diwakili oleh kelas Puck
. Bagian yang paling penting adalah metode update()
dan properti mOwner
:
1 |
public class Puck |
2 |
{
|
3 |
public var velocity :Vector3D; |
4 |
public var position :Vector3D; |
5 |
private var mOwner :Athlete; // the athlete currently carrying the puck. |
6 |
|
7 |
public function setOwner(theOwner :Athlete) :void { |
8 |
if (mOwner != theOwner) { |
9 |
mOwner = theOwner; |
10 |
velocity = null; |
11 |
}
|
12 |
}
|
13 |
|
14 |
public function update():void { |
15 |
}
|
16 |
|
17 |
public function get owner() :Athlete { return mOwner; } |
18 |
}
|
Mengikuti logika atlit yang sama, metode update()
puck akan dipandu setiap kali game terupdate. aProperti mOwner
menentukan apakah puck itu ada pada atlet manapun. Jika mOwner
maka null
, itu berarti puck "bebas", dan akan bergerak, akhirnya terpental dari arena berjalan.
Jika mOwner
tidak null
, itu berarti puck tersebut sedang dibawa oleh seorang atlet. Dalam kasus ini, akan mengabaikan pemeriksaan tabrakan dan akan ditempatkan secara paksa di depan atlet. Hal ini dapat dicapai dengan menggunakan vektor velocity
atlet, yang juga sesuai dengan arah atlet:



Vektor ahead
adalah salinan vektor velocity
atlet, sehingga mengarah ke arah yang sama. Setelah ahead
dinormalisasi, maka itu diskalakan dengan nilai apa pun-misalnya 30
-untuk mengendalikan sejauh mana pucknya akan ditempatkan di depan atlet.
Akhirnya, posisi
puck menerima posisi
atlet ditambahkan ke depan
, menempatkan puck pada posisi yang diinginkan.
Berikut adalah kode untuk semua itu:
1 |
public class Puck |
2 |
{
|
3 |
// (...)
|
4 |
|
5 |
private function placeAheadOfOwner() :void { |
6 |
var ahead :Vector3D = mOwner.boid.velocity.clone(); |
7 |
|
8 |
ahead = normalize(ahead) * 30; |
9 |
position = mOwner.boid.position + ahead; |
10 |
}
|
11 |
|
12 |
override public function update():void { |
13 |
if (mOwner != null) { |
14 |
placeAheadOfOwner(); |
15 |
}
|
16 |
}
|
17 |
|
18 |
// (...)
|
19 |
}
|
Di kelas PlayState
, ada tes tubrukan untuk memeriksa apakah puck itu tumpang tindih dengan atlit manapun. Jika ya, atlet yang baru saja menyentuh puck itu menjadi pemilik barunya. Hasilnya adalah puck yang "menempel" pada atlet. Dalam demo di bawah ini, pandu atlit untuk menyentuh puck di tengah arena untuk melihat ini beraksi:
Membuat Puck
Ini saatnya membuat puck bergerak sebagai akibat tertabrak tongkat. Terlepas dari atlit yang membawa puck, semua yang dibutuhkan untuk mensimulasikan pemukulan oleh tongkat adalah menghitung vektor kecepatan baru. Kecepatan baru itu akan menggerakkan puck ke arah tujuan yang diinginkan.
Sebuah vektor kecepatan dapat dihasilkan oleh satu vektor posisi yang lain; vektor yang baru dihasilkan kemudian akan berpindah dari satu posisi ke posisi yang lain. Itulah yang dibutuhkan untuk menghitung vektor kecepatan baru puck setelah terkena pukulan:



Pada gambar di atas, titik tujuan adalah kursor mouse. Posisi puck saat ini bisa dijadikan titik awal, sedangkan titik dimana keping harus setelah sudah terkena tongkat bisa dijadikan titik akhir.
Pseudo-code di bawah ini menunjukkan implementasi goFromStickHit()
, sebuah metode di kelas Puck
yang mengimplementasikan logika yang digambarkan pada gambar di atas:
1 |
public class Puck |
2 |
{
|
3 |
// (...)
|
4 |
|
5 |
public function goFromStickHit(theAthlete :Athlete, theDestination :Vector3D, theSpeed :Number = 160) :void { |
6 |
// Place the puck ahead of the owner to prevent unexpected trajectories
|
7 |
// (e.g. puck colliding the athlete that just hit it)
|
8 |
placeAheadOfOwner(); |
9 |
|
10 |
// Mark the puck as free (no owner)
|
11 |
setOwner(null); |
12 |
|
13 |
// Calculate the puck's new velocity
|
14 |
var new_velocity :Vector3D = theDestination - position; |
15 |
velocity = normalize(new_velocity) * theSpeed; |
16 |
}
|
17 |
}
|
Vektor new_velocity
beralih dari posisi puck saat ini ke target (theDestination
). Setelah itu, dinormalisasi dan diskalakan oleh theSpeed
, yang mendefinisikan besarnya (panjang) new_velocity
. Operasi itu, dengan kata lain, menentukan seberapa cepat puck itu akan berpindah dari posisi saat ini ke tempat tujuan. Akhirnya, vektor velocity
puck diganti dengan new_velocity
.
Di kelas PlayState
, metode goFromStichHit()
digunakan setiap kali pemain mengeklik layar. Bila itu terjadi, kursor mouse digunakan sebagai tujuan pemukulan. Hasilnya terlihat pada demo ini:
Menambahkan A.I.
Sejauh ini, kami hanya memiliki satu atlet yang bergerak di sekitar arena. Karena lebih banyak atlet yang ditambahkan, AI harus diimplementasikan untuk membuat semua atlet ini terlihat seperti mereka hidup dan berpikir.
Untuk mencapainya, kita akan menggunakan mesin kedudukan terbatas stack-based (stack-based FSM, singkatnya). Seperti yang telah dijelaskan sebelumnya, FSM sangat serbaguna dan berguna untuk menerapkan AI dalam permainan.
Untuk game hoki kami, sebuah properti bernama mBrain
akan ditambahkan ke kelas Athlete
:
1 |
public class Athlete |
2 |
{
|
3 |
// (...)
|
4 |
private var mBrain :StackFSM; // controls the AI stuff |
5 |
|
6 |
public function Athlete(thePosX :Number, thePosY :Number, theTotalMass :Number) { |
7 |
// (...)
|
8 |
mBrain = new StackFSM(); |
9 |
}
|
10 |
|
11 |
// (...)
|
12 |
}
|
Properti ini adalah contoh dari StackFSM
, sebuah kelas yang sebelumnya digunakan dalam tutorial FSM. Ini menggunakan tumpukan untuk mengendalikan kedudukan AI dari suatu entitas. Setiap kedudukan digambarkan sebagai sebuah metode; Ketika sebuah kedudukan didorong ke dalam tumpukan, itu menjadi metode aktif dan akan muncul setiap game diupdate.
Setiap kedudukan akan melakukan tugas tertentu, seperti menggerakkan atlet menuju puck. Setiap kedudukan akan bertanggung jawab untuk mengakhiri dirinya sendiri, yang berarti bertanggung jawab untuk muncul dari tumpukan.
Atlet dapat dikontrol oleh pemain atau oleh AI sekarang, jadi metode update()
di kelas Athlete
harus dimodifikasi untuk memeriksa situasi itu:
1 |
public class Athlete |
2 |
{
|
3 |
// (...)
|
4 |
|
5 |
public function update():void { |
6 |
// Clear all steering forces
|
7 |
mBoid.steering = null; |
8 |
|
9 |
if (mControlledByAI) { |
10 |
// The athlete is controlled by the AI. Update the brain (FSM) and
|
11 |
// stay away from rink walls.
|
12 |
mBrain.update(); |
13 |
|
14 |
} else { |
15 |
// The athlete is controlled by the player, so just follow
|
16 |
// the mouse cursor.
|
17 |
followMouseCursor(); |
18 |
}
|
19 |
|
20 |
// Update all steering stuff
|
21 |
mBoid.update(); |
22 |
}
|
23 |
}
|
Jika AI aktif, mBrain
akan diperbarui, yang memunculkan metode kedudukan yang sedang aktif saat ini, membuat atlet berperilaku sesuai. Jika pemain dalam keadaan dikendalikan, maka mBrain
akan diabaikan bersamaan dan atlet bergerak seakan dipandu oleh pemain.
Mengenai kedudukan yang didorong ke otak: untuk sekarang mari kita menerapkan dua keadaan. Suatu kedudukan akan membiarkan seorang atlet mempersiapkan dirinya untuk sebuah pertandingan; Saat mempersiapkan pertandingan, seorang atlet akan pindah ke posisinya di arena dan berdiri diam, menatap puck-nya. kedudukan lainnya akan membuat atlet itu berdiri diam dan menatap kepingnya.
Pada bagian berikutnya, kami akan menerapkan kedudukan-kedudukan ini.
Kedudukan diam
Jika atlet berada dalam keadaan idle
, dia akan berhenti bergerak dan menatap puck-nya. Keadaan ini digunakan saat atlit sudah berada dalam posisi di arena dan sedang menunggu sesuatu terjadi, seperti dimulainya pertandingan.
Kedudukannya akan dikodekan di kelas Athlete
, dengan metode idle()
:
1 |
public class Athlete |
2 |
{
|
3 |
// (...)
|
4 |
public function Athlete(thePosX :Number, thePosY :Number, theTotalMass :Number, theTeam :FlxGroup) { |
5 |
// (...)
|
6 |
|
7 |
// Tell the brain the current state is 'idle'
|
8 |
mBrain.pushState(idle); |
9 |
}
|
10 |
|
11 |
private function idle() :void { |
12 |
var aPuck :Puck = getPuck(); |
13 |
stopAndlookAt(aPuck.position); |
14 |
}
|
15 |
|
16 |
private function stopAndlookAt(thePoint :Vector3D) :void { |
17 |
mBoid.velocity = thePoint - mBoid.position; |
18 |
mBoid.velocity = normalize(mBoid.velocity) * 0.01; |
19 |
}
|
20 |
}
|
Karena metode ini tidak muncul dari tumpukan, ia akan tetap aktif selamanya. Pada tahap selanjutnya, kedudukan ini akan muncul untuk memberi ruang bagi kedudukan lain, seperti serangan, tapi untuk saat ini, memang inilah triknya.
Metode stopAndStareAt()
mengikuti prinsip yang sama yang digunakan untuk menghitung kecepatan puck setelah terjadi pukulan. Sebuah vektor dari posisi atlet ke posisi puck dihitung oleh thePoint - mBoid.position
dan digunakan sebagai vektor kecepatan baru atlet.
Vektor kecepatan baru itu akan menggerakkan atlet menuju puck. Untuk memastikan bahwa atlet tidak bergerak, vektornya diskalakan sebesar 0,01
, "menyusut" panjangnya menjadi hampir nol. Hal itu membuat atlet berhenti bergerak, namun tetap membuatnya menatap puck-nya.
Mempersiapkan Pertandingan
Jika atlet berada dalam keadaan prepareForMatch
, ia akan bergerak ke posisi semula, berhenti dengan mulus di sana. Posisi awalnya adalah dimana atlet harus tepat sebelum pertandingan dimulai. Karena atlit harus berhenti di tempat tujuan, metoda arrival behavior bisa digunakan lagi:
1 |
public class Athlete |
2 |
{
|
3 |
// (...)
|
4 |
private var mInitialPosition :Vector3D; // the position in the rink where the athlete should be placed |
5 |
|
6 |
public function Athlete(thePosX :Number, thePosY :Number, theTotalMass :Number, theTeam :FlxGroup) { |
7 |
// (...)
|
8 |
mInitialPosition = new Vector3D(thePosX, thePosY); |
9 |
|
10 |
// Tell the brain the current state is 'idle'
|
11 |
mBrain.pushState(idle); |
12 |
}
|
13 |
|
14 |
private function prepareForMatch() :void { |
15 |
mBoid.steering = mBoid.steering + mBoid.arrive(mInitialPosition, 80); |
16 |
|
17 |
// Am I at the initial position?
|
18 |
if (distance(mBoid.position, mInitialPosition) <= 5) { |
19 |
// I'm in position, time to stare at the puck.
|
20 |
mBrain.popState(); |
21 |
mBrain.pushState(idle); |
22 |
}
|
23 |
}
|
24 |
|
25 |
// (...)
|
26 |
}
|
Kedudukannya menggunakan metode arrival behavior untuk menggerakkan atlet menuju posisi awal. Jika jarak antara atlit dan posisi awalnya kurang dari 5
, berarti atlet sudah sampai di tempat yang diinginkan. Ketika ini terjadi, prepareForMatch
akan muncul dari tumpukan dan mendorongnya idle
, menjadikannya keadaan aktif baru.
Berikut adalah hasil penggunaan FSM berbasis tumpukan untuk mengendalikan beberapa atlet. Tekan G
untuk menempatkannya pada posisi acak di arena, mendorong kedudukan prepareForMatch
:
Kesimpulan
Tutorial ini mempresentasikan dasar-dasar untuk menerapkan permainan hoki dengan menggunakan steering behaviors and mesin stack-based finite state. Dengan menggunakan kombinasi konsep tersebut, seorang atlet dapat bergerak di arena, mengikuti kursor mouse. Atlet juga bisa memukul puck menuju tujuannya. seperti mengikuti seorang pemimpin atau mengejar lawan dengan puck.
Dengan menggunakan dua kedudukan dan FSM berbasis tumpukan, para atlet dapat mengatur ulang dan pindah ke posisi mereka di arena, untuk mempersiapkan pertandingan.
Pada tutorial selanjutnya, Anda akan belajar bagaimana membuat atlit menyerang, membawa puck ke arah tujuan sambil menghindari lawan.
Sumber
- ReferensiSprite: Hockey Stadium on GraphicRiver
- Sprites: Hockey Players by Taylor J Glidden