Scripting dan Sinematik tanpa Threading

12

Saya telah berjuang dengan cara mengimplementasikan skrip di mesin game saya. Saya hanya memiliki beberapa persyaratan: Ini harus intuitif, saya tidak ingin menulis bahasa kustom, parser dan interpreter, dan saya tidak ingin menggunakan threading. (Saya yakin ada solusi yang lebih sederhana; Saya tidak perlu kerumitan banyak untaian logika permainan.) Berikut ini contoh skrip, dengan Python (alias pseudocode):

def dramatic_scene(actors):
    alice = actors["alice"]
    bob = actors["bob"]

    alice.walk_to(bob)
    if bob.can_see(alice):
        bob.say("Hello again!")
    else:
        alice.say("Excuse me, Bob?")

Sepenggal kisah mendongeng itu menimbulkan masalah implementasi. Saya tidak bisa hanya mengevaluasi seluruh metode sekaligus, karena walk_tomembutuhkan waktu permainan. Jika kembali segera, Alice akan mulai berjalan ke Bob, dan (dalam bingkai yang sama) menyapa (atau disambut). Tetapi jika walk_toada panggilan pemblokiran yang kembali ketika dia mencapai Bob, maka permainan saya macet, karena itu memblokir alur eksekusi yang sama yang akan membuat Alice berjalan.

Saya mempertimbangkan untuk membuat setiap fungsi sebagai tindakan - alice.walk_to(bob)akan mendorong suatu objek ke antrian, yang akan muncul setelah Alice mencapai Bob, di mana pun dia berada. Itu lebih rusak: ifcabang dievaluasi segera, jadi Bob mungkin menyapa Alice bahkan jika punggungnya berbalik padanya.

Bagaimana mesin / orang lain menangani skrip tanpa membuat utas? Saya mulai mencari di area non-game-dev, seperti rantai animasi jQuery, untuk mencari ide. Sepertinya harus ada beberapa pola yang baik untuk masalah seperti ini.

ojrac
sumber
1
+1 untuk pertanyaan tetapi terutama untuk "Python (alias pseudocode)" :)
Ricket
Python seperti memiliki kekuatan super.
ojrac

Jawaban:

3

Cara sesuatu seperti Panda melakukan ini adalah dengan callback. Alih-alih memblokir itu akan menjadi sesuatu seperti

def dramatic_scene(actors):
    alice = actors["alice"]
    bob = actors["bob"]

    def cb():
        if bob.can_see(alice):
            bob.say("Hello again!")
        else:
            alice.say("Excuse me, Bob?")
    alice.walk_to(bob, cb)

Memiliki panggilan balik yang lengkap memungkinkan Anda untuk menghubungkan peristiwa semacam ini sedalam yang Anda inginkan.

EDIT: Contoh JavaScript karena memiliki sintaks yang lebih baik untuk gaya ini:

function dramatic_scene(actors) {
    var alice = actors.alice;
    var bob = actors.bob;
    alice.walk_to(bob, function() {
        if(bob.can_see(alice)) {
            bob.say('Hello again!');
        } else {
            alice.say('Excuse me, Bob?');
        }
     });
}
pembuat kode
sumber
Ini cukup berfungsi untuk +1, saya pikir itu salah untuk mendesain konten. Saya bersedia melakukan beberapa pekerjaan ekstra pada akhir saya sehingga skrip terlihat sederhana dan jelas.
ojrac
1
@ojrac: Sebenarnya cara ini lebih baik, karena di sini dalam skrip yang sama Anda dapat memberi tahu banyak aktor untuk mulai berjalan pada saat yang sama.
Bart van Heukelom
Ugh, poin bagus.
ojrac
Untuk meningkatkan keterbacaan, definisi panggilan balik dapat disarangkan di dalam panggilan walk_to (), atau dimasukkan setelahnya (untuk keduanya berlaku: jika bahasa mendukung) sehingga kode yang dipanggil nanti terlihat nanti di sumbernya.
Bart van Heukelom
Sayangnya, Python tidak terlalu bagus untuk sintaksis semacam ini. Ini terlihat jauh lebih bagus dalam JavaScript (lihat di atas, tidak dapat menggunakan pemformatan kode di sini).
coderanger
13

Istilah yang ingin Anda cari di sini adalah " coroutines " (dan biasanya kata kunci bahasa atau nama fungsinya yield).

Coroutine adalah komponen program yang menggeneralisasi subrutin untuk memungkinkan beberapa titik masuk untuk menunda dan melanjutkan eksekusi di lokasi tertentu.

Implementasi akan tergantung pertama-tama pada bahasa Anda. Untuk gim Anda ingin implementasinya seberat mungkin (lebih ringan dari benang atau bahkan serat). Halaman Wikipedia (tertaut) memiliki beberapa tautan ke berbagai implementasi khusus bahasa.

Saya mendengar Lua memiliki dukungan bawaan untuk coroutine. Begitu juga GameMonkey.

UnrealScript mengimplementasikan ini dengan apa yang disebutnya "status" dan "fungsi laten".

Jika Anda menggunakan C # Anda bisa melihat posting blog ini oleh Nick Gravelyn.

Selain itu, ide "rantai animasi", meskipun bukan hal yang sama, adalah solusi yang bisa diterapkan untuk masalah yang sama. Nick Gravelyn juga memiliki implementasi C # ini .

Andrew Russell
sumber
Tangkapan bagus, Tetrad;)
Andrew Russell
Ini sangat bagus, tapi saya tidak yakin itu membuat saya 100% dari perjalanan ke sana. Sepertinya coroutine memungkinkan Anda menghasilkan hingga metode pemanggilan, tapi saya ingin cara untuk menghasilkan dari skrip Lua, sepanjang jalan menumpuk ke kode C # tanpa menulis sementara (walk_to ()! = Selesai) {yield}.
ojrac
@ojrac: Saya tidak tahu tentang Lua, tetapi jika Anda menggunakan metode C # Nick Gravelyn, Anda dapat mengembalikan delegasi (atau objek yang berisi satu) yang memegang syarat bagi manajer skrip Anda untuk memeriksa (kode Nick hanya mengembalikan waktu yang secara implisit merupakan syarat). Anda bahkan dapat memiliki fungsi laten sendiri mengembalikan delegasi, sehingga Anda dapat menulis: yield return walk_to();di skrip Anda.
Andrew Russell
Hasil di C # luar biasa, tapi saya mengoptimalkan solusi saya untuk skrip sederhana, sulit-untuk-berantakan. Saya akan lebih mudah menjelaskan callback daripada menghasilkan, jadi saya akan menerima jawaban yang lain. Saya akan +2 jika saya bisa.
ojrac
1
Anda biasanya tidak perlu menjelaskan panggilan hasil - Anda dapat membungkusnya di fungsi "walk_to", misalnya.
Kylotan
3

tidak akan berulir cerdas.

Sebagian besar mesin gim bekerja sebagai serangkaian tahap modular dengan hal-hal di memori yang mendorong setiap tahap. Untuk 'walk to example' Anda, Anda biasanya memiliki tahap AI di mana karakter Anda berjalan dalam keadaan di mana mereka tidak harus mencari musuh untuk dibunuh, tahap animasi di mana mereka harus menjalankan animasi X, tahap fisika (atau tahap simulasi) di mana posisi aktualnya diperbarui, dll.

dalam contoh Anda di atas, 'alice' adalah aktor yang terdiri dari potongan-potongan yang hidup di banyak tahapan ini, jadi aktor yang menghalangi. untuk membuat banyak keputusan.

Sebagai gantinya, fungsi 'start_walk_to' mungkin akan melakukan sesuatu seperti:

def start_cutscene_walk_to(actor,target):
    actor.ai.setbrain(cutscene_brain)
    actor.physics.nocoll = 1
    actor.anims.force_anim('walk')
    # etc.

Kemudian, loop utama Anda menjalankan tanda ai, tanda fisika, tanda animasi, dan tanda cutscene, dan cutscene memperbarui status untuk setiap subsistem di mesin Anda. Sistem cutscene pasti harus melacak apa yang masing-masing cutscene lakukan, dan sistem yang didorong coroutine untuk sesuatu yang linier dan deterministik seperti custscene mungkin masuk akal.

Alasan untuk modularitas ini adalah bahwa hal itu hanya membuat hal-hal bagus dan sederhana, dan untuk beberapa sistem (seperti fisika dan AI), Anda perlu mengetahui keadaan segalanya pada saat yang sama untuk menyelesaikan sesuatu dengan benar dan menjaga permainan dalam keadaan konsisten .

semoga ini membantu!

Aaron Brady
sumber
Saya suka dengan apa yang Anda tuju, tetapi saya benar-benar merasa kuat tentang nilai actor.walk_to ([aktor lain, posisi tetap, atau bahkan fungsi yang mengembalikan posisi]). Tujuan saya adalah menyediakan alat yang sederhana dan mudah dipahami dan menangani semua kerumitan dari bagian pembuatan konten permainan. Anda juga membantu saya menyadari bahwa semua yang saya inginkan adalah cara untuk memperlakukan setiap skrip sebagai mesin negara yang terbatas.
ojrac
senang saya bisa membantu! Saya merasa tanggapan saya sedikit di luar topik :) pasti setuju dengan nilai fungsi actor.walk_to untuk mencapai tujuan Anda, saya menantikan untuk mendengar tentang implementasi Anda.
Aaron Brady
Sepertinya saya akan menggunakan campuran gaya panggilan balik jQuery dan fungsi berantai. Lihat jawaban yang diterima;)
ojrac