Dalam posting ini saya akan berbicara tentang bagaimana Anda dapat menerapkan urutan aksi dan adegan cut dalam video game. Artikel ini adalah terjemahan dari artikel ini dan pada topik yang sama saya membuat presentasi di Lua di Moskow, jadi jika Anda lebih suka menonton video, Anda dapat menontonnya di sini .
Kode artikel ditulis dalam Lua, tetapi dapat dengan mudah ditulis dalam bahasa lain (dengan pengecualian metode yang menggunakan coroutine, karena mereka tidak dalam semua bahasa).
Artikel ini menunjukkan cara membuat mekanisme yang memungkinkan Anda untuk menulis adegan cutscene dari formulir berikut:
local function cutscene(player, npc) player:goTo(npc) if player:hasCompleted(quest) then npc:say("You did it!") delay(0.5) npc:say("Thank you") else npc:say("Please help me") end end
Entri
Urutan tindakan sering ditemukan di video game. Misalnya, dalam adegan cutscenes: karakter bertemu musuh, mengatakan sesuatu kepadanya, musuh menjawab, dan sebagainya. Urutan tindakan dapat ditemukan di gameplay. Lihatlah gif ini:

1. Pintu terbuka
2. Karakter memasuki rumah
3. Pintu tertutup
4. Layar secara bertahap menjadi gelap
5. Levelnya berubah
6. Layar memudar dengan cerah
7. Karakter memasuki kafe
Urutan tindakan juga dapat digunakan untuk skrip perilaku NPC atau untuk menerapkan pertempuran bos di mana bos melakukan beberapa tindakan satu demi satu.
Masalah
Struktur loop permainan standar membuat penerapan urutan tindakan sulit. Katakanlah kita memiliki lingkaran permainan berikut:

while game:isRunning() do processInput() dt = clock.delta() update(dt) render() end
Kami ingin menerapkan cutscene berikut: pemain mendekati NPC, NPC mengatakan: "Anda berhasil!", Dan kemudian setelah jeda singkat mengatakan: "Terima kasih!". Di dunia yang ideal, kita akan menulisnya seperti ini:
player:goTo(npc) npc:say("You did it!") delay(0.5) npc:say("Thank you")
Dan di sini kita dihadapkan dengan masalah. Butuh beberapa waktu untuk menyelesaikan langkah-langkah ini. Beberapa tindakan bahkan mungkin menunggu masukan dari pemain (misalnya, untuk menutup kotak dialog). Alih-alih fungsi
delay
, Anda tidak dapat memanggil
sleep
sama - ini akan terlihat seperti permainan dibekukan.
Mari kita lihat beberapa pendekatan untuk menyelesaikan masalah.
bool, enum, mesin negara
Cara yang paling jelas untuk menerapkan urutan tindakan adalah menyimpan informasi tentang keadaan saat ini dalam bools, string, atau enum. Kode akan terlihat seperti ini:
function update(dt) if cutsceneState == 'playerGoingToNpc' then player:continueGoingTo(npc) if player:closeTo(npc) then cutsceneState = 'npcSayingYouDidIt' dialogueWindow:show("You did it!") end elseif cutsceneState == 'npcSayingYouDidIt' then if dialogueWindow:wasClosed() then cutsceneState = 'delay' end elseif ... ...
Pendekatan ini dengan mudah mengarah pada kode spaghetti dan rantai panjang ekspresi if-else, jadi saya sarankan menghindari metode pemecahan masalah ini.
Daftar tindakan
Daftar tindakan sangat mirip dengan mesin negara. Daftar tindakan adalah daftar tindakan yang dilakukan satu demi satu. Dalam loop game, fungsi
update
dipanggil untuk tindakan saat ini, yang memungkinkan kami untuk memproses input dan membuat game, bahkan jika tindakan itu membutuhkan waktu lama. Setelah tindakan selesai, kami melanjutkan ke yang berikutnya.
Dalam cutscene yang ingin kami terapkan, kami perlu mengimplementasikan tindakan berikut: GoToAction, DialogueAction, dan DelayAction.
Untuk contoh lebih lanjut, saya akan menggunakan pustaka
middleclass untuk OOP di Lua.
Begini cara
DelayAction
diimplementasikan:
ActionList:update
fungsi
ActionList:update
terlihat seperti ini:
function ActionList:update(dt) if not self.isFinished then self.currentAction:update(dt) if self.currentAction.isFinished then self:goToNextAction() if not self.currentAction then self.isFinished = true end end end end
Dan akhirnya, implementasi cutscene itu sendiri:
function makeCutsceneActionList(player, npc) return ActionList:new { GoToAction:new { entity = player, target = npc }, SayAction:new { entity = npc, text = "You did it!" }, DelayAction:new { delay = 0.5 }, SayAction:new { entity = npc, text = "Thank you" } } end
Catatan : di Lua, panggilan ke
someFunction({ ... })
dapat dibuat seperti ini:
someFunction{...}
. Ini memungkinkan Anda untuk menulis
DelayAction:new{ delay = 0.5 }
alih-alih
DelayAction:new({delay = 0.5})
.
Terlihat jauh lebih baik. Kode dengan jelas menunjukkan urutan tindakan. Jika kita ingin menambahkan tindakan baru, kita dapat dengan mudah melakukannya. Sangat mudah untuk membuat kelas seperti
DelayAction
untuk membuat cutscene penulisan lebih nyaman.
Saya menyarankan Anda untuk melihat presentasi Sean Middleditch tentang daftar tindakan, yang memberikan contoh yang lebih kompleks.
Daftar tindakan umumnya sangat bermanfaat. Saya menggunakan mereka untuk permainan saya untuk beberapa waktu dan secara keseluruhan senang. Namun pendekatan ini juga memiliki kelemahan. Katakanlah kita ingin menerapkan cutscene yang sedikit lebih kompleks:
local function cutscene(player, npc) player:goTo(npc) if player:hasCompleted(quest) then npc:say("You did it!") delay(0.5) npc:say("Thank you") else npc:say("Please help me") end end
Untuk melakukan simulasi if / else, Anda perlu mengimplementasikan daftar non-linear. Ini dapat dilakukan dengan menggunakan tag. Beberapa tindakan dapat ditandai, dan kemudian, dengan beberapa syarat, alih-alih pindah ke tindakan berikutnya, Anda bisa pergi ke tindakan yang memiliki tag yang diinginkan. Ini berfungsi, namun tidak mudah untuk membaca dan menulis seperti fungsi di atas.
Lua coroutine membuat kode ini menjadi kenyataan.
Coroutine
Dasar-dasar Corua di Lua
Corutin adalah fungsi yang dapat dijeda dan kemudian dilanjutkan. Coroutine dieksekusi di utas yang sama dengan program utama. Tidak ada utas baru yang pernah dibuat untuk coroutine.
Untuk menjeda
coroutine.yield
, Anda perlu menghubungi
coroutine.yield
, untuk melanjutkan -
coroutine.resume
. Contoh sederhana:
local function f() print("hello") coroutine.yield() print("world!") end local c = coroutine.create(f) coroutine.resume(c) print("uhh...") coroutine.resume(c)
Output Program:
halo
uhh ...
dunia
Begini cara kerjanya. Pertama, kita membuat
coroutine.create
menggunakan
coroutine.create
. Setelah panggilan ini, corutin tidak mulai. Agar ini terjadi, kita perlu menjalankannya menggunakan
coroutine.resume
. Kemudian fungsi
f
disebut, yang menulis "halo" dan menjeda dirinya sendiri dengan
coroutine.yield
. Ini mirip dengan
return
, tetapi kita dapat melanjutkan
f
dengan
coroutine.resume
.
Jika Anda melewatkan argumen saat memanggil
coroutine.yield
, maka mereka akan menjadi nilai balik dari panggilan yang sesuai ke
coroutine.resume
di "aliran utama".
Sebagai contoh:
local function f() ... coroutine.yield(42, "some text") ... end ok, num, text = coroutine.resume(c) print(num, text)
ok
adalah variabel yang memungkinkan kita untuk mengetahui status coroutine. Jika
ok
true
, maka dengan coroutine semuanya baik-baik saja, tidak ada kesalahan terjadi di dalam. Nilai-nilai pengembalian yang mengikutinya (
num
,
text
) adalah argumen yang sama yang kami berikan untuk
yield
.
Jika
ok
false
, maka ada yang salah dengan coroutine, misalnya, fungsi
error
dipanggil di dalamnya. Dalam hal ini, nilai pengembalian kedua akan menjadi pesan kesalahan. Contoh coroutine di mana kesalahan terjadi:
local function f() print(1 + notDefined) end c = coroutine.create(f) ok, msg = coroutine.resume(c) if not ok then print("Coroutine failed!", msg) end
Kesimpulan:
Coroutine gagal! input: 4: upaya untuk melakukan aritmatika pada nilai nol (global 'notDefined')
Status
coroutine.status
dapat diperoleh dengan memanggil
coroutine.status
. Corutin mungkin dalam kondisi berikut:
- "Running" - Coroutine sedang berjalan.
coroutine.status
dipanggil dari corutin itu sendiri - "Ditangguhkan" - Corutin dijeda atau belum pernah dimulai
- "Normal" - corutin aktif, tetapi tidak dieksekusi. Artinya, corutin meluncurkan corutin lain di dalam dirinya
- "Mati" - eksekusi coroutine selesai (yaitu, fungsi dalam coroutine selesai)
Sekarang, dengan bantuan pengetahuan ini, kita dapat menerapkan sistem urutan tindakan dan adegan cutscene berdasarkan coroutine.
Membuat cutscene menggunakan corutin
Inilah yang akan terlihat seperti kelas
Action
pada sistem baru:
function Action:launch() self:init() while not self.finished do local dt = coroutine.yield() self:update(dt) end self:exit() end
Pendekatannya mirip dengan daftar tindakan: fungsi
update
dari tindakan dipanggil hingga tindakan selesai. Tapi di sini kita menggunakan coroutine dan
yield
di setiap iterasi dari loop game (
Action:launch
dipanggil dari beberapa coroutine). Di suatu tempat dalam
update
loop game, kami melanjutkan eksekusi cutscene saat ini seperti ini:
coroutine.resume(c, dt)
Dan akhirnya, membuat cutscene:
function cutscene(player, npc) player:goTo(npc) npc:say("You did it!") delay(0.5) npc:say("Thank you") end
Inilah cara fungsi
delay
diimplementasikan:
function delay(time) action = DelayAction:new { delay = time } action:launch() end
Membuat pembungkus seperti itu sangat meningkatkan keterbacaan kode cutscene.
DelayAction
diimplementasikan seperti ini:
Implementasi ini identik dengan yang kami gunakan dalam daftar tindakan! Mari kita lihat
Action:launch
fungsi
Action:launch
lagi:
function Action:launch() self:init() while not self.finished do local dt = coroutine.yield()
Hal utama di sini adalah
while
, yang berjalan hingga aksi selesai. Itu terlihat seperti ini:

Sekarang mari kita lihat fungsi
goTo
:
function Entity:goTo(target) local action = GoToAction:new { entity = self, target = target } action:launch() end function GoToAction:initialize(params) ... end function GoToAction:update(dt) if not self.entity:closeTo(self.target) then ...
Coroutine berjalan baik dengan berbagai acara. Menerapkan kelas
WaitForEventAction
:
function WaitForEventAction:initialize(params) self.finished = false eventManager:subscribe { listener = self, eventType = params.eventType, callback = WaitForEventAction.onEvent } end function WaitForEventAction:onEvent(event) self.finished = true end
Fungsi ini tidak memerlukan metode
update
. Itu akan dieksekusi (meskipun tidak akan melakukan apa-apa ...) sampai menerima acara dengan jenis yang diperlukan. Ini adalah aplikasi praktis dari kelas ini - implementasi fungsi
say
:
function Entity:say(text) DialogueWindow:show(text) local action = WaitForEventAction:new { eventType = 'DialogueWindowClosed' } action:launch() end
Sederhana dan mudah dibaca. Ketika kotak dialog ditutup, ia mengirimkan acara bertipe 'DialogueWindowClosed`. Tindakan say berakhir dan yang berikutnya mulai dijalankan.
Menggunakan coroutine, Anda dapat dengan mudah membuat cutscenes nonlinear dan pohon dialog:
local answer = girl:say('do_you_love_lua', { 'YES', 'NO' }) if answer == 'YES' then girl:setMood('happy') girl:say('happy_response') else girl:setMood('angry') girl:say('angry_response') end

Dalam contoh ini, fungsi
say
sedikit lebih kompleks daripada yang saya tunjukkan sebelumnya. Ini mengembalikan pilihan pemain dalam dialog, namun tidak sulit untuk diterapkan. Misalnya,
WaitForEventAction
dapat digunakan secara
WaitForEventAction
, yang akan menangkap acara PlayerChoiceEvent dan kemudian mengembalikan pilihan pemain yang informasinya akan terkandung dalam objek acara.
Contoh yang sedikit lebih rumit
Dengan coroutine, Anda dapat dengan mudah membuat tutorial dan pencarian kecil. Sebagai contoh:
girl:say("Kill that monster!") waitForEvent('EnemyKilled') girl:setMood('happy') girl:say("You did it! Thank you!")

Coroutine juga dapat digunakan untuk AI. Misalnya, Anda dapat membuat fungsi yang akan membuat monster bergerak di sepanjang lintasan:
function followPath(monster, path) local numberOfPoints = path:getNumberOfPoints() local i = 0

Ketika monster melihat pemain, kita bisa berhenti memenuhi coroutine dan menghapusnya. Oleh karena itu, loop tak terbatas (
while true
) di dalam
followPath
tidak benar-benar tak terbatas.
Dengan corutin, Anda dapat melakukan tindakan "paralel". Cutscene tidak akan melanjutkan ke tindakan berikutnya sampai kedua tindakan selesai. Sebagai contoh, kami akan membuat cutscene di mana seorang gadis dan kucing pergi ke titik teman dengan kecepatan yang berbeda. Setelah mereka mendatanginya, kucing itu berkata "mengeong".
function cutscene(cat, girl, meetingPoint) local c1 = coroutine.create( function() cat:goTo(meetingPoint) end) local c2 = coroutine.create( function() girl:goTo(meetingPoint) end) c1.resume() c2.resume()
Bagian terpenting di sini adalah fungsi
waitForFinish
, yang merupakan pembungkus di sekitar kelas
WaitForFinishAction
, yang dapat diimplementasikan sebagai berikut:
function WaitForFinishAction:update(dt) if coroutine.status(self.c1) == 'dead' and coroutine.status(self.c2) == 'dead' then self.finished = true else if coroutine.status(self.c1) ~= 'dead' then coroutine.resume(self.c1, dt) end if coroutine.status(self.c2) ~= 'dead' then coroutine.resume(self.c2, dt) end end
Anda dapat membuat kelas ini lebih kuat jika Anda mengizinkan sinkronisasi jumlah tindakan ke-N.
Anda juga dapat membuat kelas yang akan menunggu sampai
salah satu coroutine selesai, alih-alih menunggu
semua coroutine untuk menyelesaikan eksekusi. Misalnya, dapat digunakan dalam balap mini-game. Di dalam coroutine akan ada menunggu salah satu pembalap untuk mencapai garis finish dan kemudian melakukan beberapa tindakan.
Keuntungan dan kerugian dari corutin
Coroutine adalah mekanisme yang sangat berguna. Dengan menggunakannya Anda dapat menulis cutscene dan kode gameplay yang mudah dibaca dan dimodifikasi. Adegan seperti ini dapat dengan mudah ditulis oleh modder atau orang yang bukan programmer (misalnya, desainer game atau level).
Dan semua ini dilakukan dalam satu utas, sehingga tidak ada masalah dengan sinkronisasi atau
kondisi balapan .
Pendekatan ini memiliki kelemahan. Misalnya, mungkin ada masalah dengan save. Katakanlah permainan Anda memiliki tutorial panjang yang diimplementasikan dengan coroutine. Selama tutorial ini pemain tidak akan dapat menyimpan karena Untuk melakukan ini, Anda harus menyimpan keadaan saat ini dari coroutine (yang mencakup seluruh tumpukan dan nilai-nilai variabel di dalamnya), sehingga setelah memuat lebih lanjut dari save, Anda dapat melanjutkan tutorial.
(
Catatan : menggunakan perpustakaan
PlutoLibrary, coroutine dapat diserialisasi, tetapi perpustakaan hanya berfungsi dengan Lua 5.1)
Masalah ini tidak terjadi dengan cutscene, seperti biasanya dalam game di tengah cutscene tidak diperbolehkan.
Masalah dengan tutorial yang panjang dapat diselesaikan jika Anda memecahnya menjadi potongan-potongan kecil. Misalkan seorang pemain melewati bagian pertama tutorial dan harus pergi ke ruangan lain untuk melanjutkan tutorial. Pada titik ini, Anda dapat membuat pos pemeriksaan atau memberikan pemain kesempatan untuk menabung. Dalam save, kami akan menulis sesuatu seperti "pemain menyelesaikan bagian 1 dari tutorial". Selanjutnya, pemain akan melalui bagian kedua tutorial, yang mana kita sudah akan menggunakan coroutine lain. Dan seterusnya ... Saat memuat, kami baru mulai menjalankan coroutine yang sesuai dengan bagian yang harus dilalui pemain.
Kesimpulan
Seperti yang dapat Anda lihat, ada beberapa pendekatan berbeda untuk mengimplementasikan serangkaian tindakan dan adegan adegan. Tampaknya bagi saya bahwa pendekatan coroutine sangat kuat dan saya senang membagikannya kepada para pengembang. Saya harap solusi masalah ini akan membuat hidup Anda lebih mudah dan memungkinkan Anda membuat adegan-adegan epik dalam gim Anda.