Plant adalah pengalaman referensi di mana pemain menanam dan menyiram benih, sehingga mereka dapat memanen dan menjual tanaman yang dihasilkan nanti.

Proyek ini berfokus pada kasus penggunaan umum yang mungkin Anda temui saat mengembangkan pengalaman di Roblox. Jika memungkinkan, Anda akan menemukan catatan tentang tradeoff, kompromi, dan alasan berbagai pilihan implementasi, sehingga Anda dapat membuat keputusan terbaik untuk pengalaman Anda sendiri.
Dapatkan file
- Arahkan ke halaman pengalaman Tanaman.
- Klik tombol ⋯ dan Edit di Studio.
Kasus penggunaan
Plant mencakup kasus penggunaan berikut:
- Persistensi data sesi dan data pemain
- Manajemen tampilan UI
- Jaringan klien-server
- Pengalaman Pengguna Pertama Kali (FTUE)
- Pembelian mata uang keras dan lunak
Selain itu, proyek ini juga menyelesaikan seperangkat masalah yang lebih sempit yang berlaku untuk banyak pengalaman, termasuk:
- Kustomisasi area dalam tempat yang diasosiasikan dengan pemain
- Mengelola kecepatan gerak karakter pemain
- Membuat objek yang mengikuti karakter
- Mendeteksi bagian dunia di mana karakter berada
Perlu dicatat bahwa ada beberapa kasus penggunaan dalam pengalaman ini yang terlalu kecil, terlalu niche, atau tidak menunjukkan solusi untuk tantangan desain yang menarik; ini tidak dicakup.
Struktur proyek
Keputusan pertama saat membuat pengalaman adalah menentukan bagaimana cara menyusun proyek, yang terutama mencakup di mana menempatkan instansi tertentu dalam model data dan bagaimana mengorganisir serta menyusun titik masuk untuk kode klien dan server.
Model data
Tabel berikut menjelaskan layanan wadah mana dalam model data di mana instansi diletakkan.
| Layanan | Jenis instansi |
|---|---|
| Workspace | Berisi model statis yang mewakili dunia 3D, khususnya bagian dunia yang tidak dimiliki oleh pemain mana pun. Anda tidak perlu membuat, memodifikasi, atau menghancurkan instansi ini secara dinamis saat runtime, jadi sah untuk membiarkannya di sini. Selain itu, ada Folder kosong, di mana model pertanian pemain akan ditambahkan saat runtime. |
| Lighting | Efek atmosfer dan pencahayaan. |
| ReplicatedFirst | Berisi subset terkecil dari instansi yang diperlukan untuk menampilkan layar pemuatan dan menginisialisasi pengalaman. Semakin banyak instansi yang ditempatkan di ReplicatedFirst, semakin lama waktu tunggu untuk mereplikasi sebelum kode di ReplicatedFirst dapat dijalankan.
|
| ReplicatedStorage | Berfungsi sebagai wadah penyimpanan untuk semua instansi yang memerlukan akses di kedua sisi, klien dan server.
|
| ServerScriptService | Berisi Script yang berfungsi sebagai titik masuk untuk semua kode sisi server dalam proyek. |
| ServerStorage | Berfungsi sebagai wadah penyimpanan untuk semua instansi yang tidak perlu direplikasi ke klien.
|
| SoundService | Berisi objek Sound yang digunakan untuk efek suara dalam pengalaman. Di bawah SoundService, objek Sound ini tidak memiliki posisi dan tidak disimulasikan dalam ruang 3D. |
Titik masuk
Sebagian besar proyek mengorganisir kode di dalam ModuleScripts yang dapat digunakan kembali yang dapat diimpor di seluruh basis kode. ModuleScripts dapat digunakan kembali namun tidak dieksekusi sendiri; mereka perlu diimpor oleh Script atau LocalScript. Banyak proyek Roblox akan memiliki sejumlah besar objek Script dan LocalScript, masing-masing berkaitan dengan perilaku atau sistem tertentu dalam pengalaman, sehingga menciptakan banyak titik masuk.
Untuk mikrogame Plant, pendekatan yang berbeda diterapkan melalui satu LocalScript yang menjadi titik masuk untuk semua kode klien, dan satu Script yang menjadi titik masuk untuk semua kode server. Pendekatan yang benar untuk proyek Anda tergantung pada kebutuhan Anda, tetapi satu titik masuk memberikan kontrol yang lebih besar atas urutan di mana sistem dieksekusi.
Daftar berikut menjelaskan tradeoff dari kedua pendekatan:
- Satu Script dan satu LocalScript mencakup kode server dan klien masing-masing.
- Kontrol yang lebih besar atas urutan di mana berbagai sistem dimulai karena semua kode diinisialisasi dari satu skrip.
- Dapat mengoper objek dengan referensi antara sistem.
Arsitektur sistem tingkat tinggi
Sistem tingkat atas dalam proyek dijelaskan di bawah. Beberapa sistem ini secara substansial lebih kompleks daripada yang lain, dan dalam banyak kasus fungsionalitas mereka terabstraksi melalui hierarki kelas lain.

Setiap sistem ini adalah "singleton," dalam artian bahwa ini adalah kelas yang tidak dapat diinstansiasi yang sebaliknya diinisialisasi oleh skrip start klien atau server yang relevan. Anda dapat membaca lebih lanjut tentang pola singleton di kemudian hari dalam panduan ini.
Server
Sistem berikut terkait dengan server.
| Sistem | Deskripsi |
|---|---|
| Jaringan |
|
| PlayerDataServer |
|
| Pasar |
|
| CollisionGroupManager |
|
| FarmManagerServer |
|
| PlayerObjectsContainer |
|
| TagPlayers |
|
| FtueManagerServer |
|
| CharacterSpawner |
|
Klien
Sistem berikut terkait dengan klien.
| Sistem | Deskripsi |
|---|---|
| Jaringan |
|
| PlayerDataClient |
|
| MarketClient |
|
| LocalWalkJumpManager |
|
| FarmManagerClient |
|
| UISetup |
|
| FtueManagerClient |
|
| CharacterSprint |
|
Komunikasi klien-server
Sebagian besar pengalaman di Roblox melibatkan beberapa elemen komunikasi antara klien dan server. Ini dapat mencakup klien yang meminta server untuk melakukan tindakan tertentu dan server yang mereplikasi pembaruan ke klien.
Dalam proyek ini, komunikasi klien-server dijaga agar tetap umum mungkin dengan membatasi penggunaan objek RemoteEvent dan RemoteFunction untuk mengurangi jumlah aturan khusus yang perlu dilacak. Proyek ini menggunakan metode berikut, dalam urutan preferensi:
- Replikasi melalui sistem data pemain.
- Replikasi melalui atribut.
- Replikasi melalui tag.
- Mengirim pesan langsung melalui modul Jaringan.
Replikasi melalui sistem data pemain
sistem data pemain memungkinkan data untuk diasosiasikan dengan pemain yang berlanjut antara sesi penyimpanan. Sistem ini menyediakan replikasi dari klien ke server dan seperangkat API yang dapat digunakan untuk melakukan query data dan berlangganan ke perubahan, menjadikannya ideal untuk mereplikasi perubahan status pemain dari server ke klien.
Sebagai contoh, daripada memicu UpdateCoins RemoteEvent untuk memberitahu klien berapa banyak koin yang dimilikinya, Anda dapat memanggil yang berikut dan membiarkan klien berlangganan kepadanya melalui peristiwa PlayerDataClient.updated.
PlayerDataServer.setValue(player, "coins", 5)
Tentu saja, ini hanya berguna untuk replikasi server-ke-klien dan untuk nilai yang ingin Anda pertahankan antara sesi, tetapi ini berlaku untuk sejumlah kasus yang mengejutkan dalam proyek, termasuk:
- Tahap FTUE saat ini
- Inventaris pemain
- Jumlah koin yang dimiliki pemain
- Status pertanian pemain
Replikasi melalui atribut
Dalam situasi di mana server perlu mereplikasi nilai kustom ke klien yang spesifik untuk Instance tertentu, Anda dapat menggunakan atribut. Roblox secara otomatis mereplikasi nilai atribut, jadi Anda tidak perlu mempertahankan jalur kode apa pun untuk mereplikasi status yang terkait dengan objek. Keuntungan lain adalah bahwa replikasi ini terjadi bersamaan dengan instansi itu sendiri.
Ini sangat berguna untuk instansi yang dibuat pada runtime, karena atribut yang ditetapkan pada instansi baru sebelum diparentkan ke model data akan mereplikasi secara atomik dengan instansi itu sendiri. Ini menghindari kebutuhan untuk menulis kode untuk "menunggu" data tambahan untuk direplikasi melalui RemoteEvent atau StringValue.
Anda juga dapat langsung membaca atribut dari model data, baik dari klien atau server, dengan metode GetAttribute() dan berlangganan ke perubahan dengan GetAttributeChangedSignal() method. Dalam proyek Plant, pendekatan ini digunakan untuk, di antara hal-hal lain, mereplikasi status terkini tanaman kepada klien.
Replikasi melalui tag
CollectionService memungkinkan Anda menerapkan tag string ke Instance. Ini berguna untuk mengkategorikan instansi dan mereplikasi kategorisasi itu ke klien.
Sebagai contoh, tag CanPlant diterapkan di server untuk menandai kepada klien bahwa pot tertentu dapat menerima tanaman.
Mengirim pesan langsung melalui modul jaringan
Untuk situasi di mana tidak ada opsi sebelumnya yang dapat diterapkan, Anda dapat menggunakan panggilan jaringan kustom melalui modul Jaringan. Ini adalah satu-satunya opsi dalam proyek yang memungkinkan komunikasi klien-ke-server dan oleh karena itu paling berguna untuk mentransmisikan permintaan klien dan menerima respons server.
Plant menggunakan panggilan jaringan langsung untuk berbagai permintaan klien, termasuk:
- Menyiram tanaman
- Menanam benih
- Membeli item
Kekurangan dari pendekatan ini adalah bahwa setiap pesan individual memerlukan beberapa konfigurasi khusus yang dapat meningkatkan kompleksitas proyek, meskipun ini telah dihindari sebisa mungkin, terutama untuk komunikasi server-ke-klien.
Kelas dan singleton
Kelas dalam proyek Plant, seperti instansi di Roblox, dapat dibuat dan dihancurkan. Sintaks kelasnya terinspirasi oleh pendekatan idiomatik Lua untuk pemrograman berorientasi objek dengan sejumlah perubahan untuk mendukung dukungan typechecking ketat.
Instansiasi
Banyak kelas dalam proyek ini terkait dengan satu atau lebih Instances. Objek dari kelas tertentu dibuat menggunakan metode new() yang konsisten dengan cara instansi dibuat di Roblox menggunakan Instance.new().
Pola ini umumnya digunakan untuk objek di mana kelas memiliki representasi fisik dalam model data, dan kelas memperluas fungsionalitasnya. Contoh yang baik adalah BeamBetween yang menciptakan objek Beam antara dua objek Attachment yang diberikan dan menjaga agar lampiran tersebut berorientasi sehingga berkas selalu menghadap ke atas. Instansi ini bisa jadi diklon dari versi prefabrikasi di ReplicatedStorage atau diteruskan ke new() sebagai argumen dan disimpan di dalam objek di bawah self.
Instansi yang sesuai
Seperti yang disebutkan di atas, banyak kelas dalam proyek ini memiliki representasi model data, sebuah instansi yang sesuai dengan kelas dan dimanipulasi olehnya.
Alih-alih membuat instansi ini saat objek kelas diinstansiasi, kode umumnya memilih untuk Clone() versi prefabrikasi dari Instance yang disimpan di bawah ReplicatedStorage atau ServerStorage. Meskipun mungkin untuk menyerialisasi properti dari instansi- instansi ini dan membuatnya dari awal di fungsi new() kelas, melakukannya akan membuat pengeditan objek menjadi sangat rumit dan membuatnya lebih sulit bagi pembaca untuk memahami. Selain itu, mengkloning instansi umumnya adalah operasi yang lebih cepat daripada membuat instansi baru dan mengustomisasi propertinya saat runtime.
Komposisi
Meskipun pewarisan mungkin dilakukan di Luau menggunakan metatables, proyek ini memilih untuk memungkinkan kelas untuk memperluas satu sama lain melalui komposisi. Ketika menggabungkan kelas melalui komposisi, objek "anak" diinstansiasi dalam metode new() kelas dan dimasukkan sebagai anggota di bawah self.
Sebagai contoh dari hal ini dalam tindakan, lihat kelas CloseButton yang membungkus kelas Button.
Pembersihan
Serupa dengan bagaimana Instance dapat dihancurkan dengan metode Destroy(), kelas yang dapat diinstansiasi juga dapat dihancurkan. Metode destruktor untuk kelas proyek adalah destroy() dengan huruf kecil d untuk konsistensi camelCase di antara metode dalam basis kode, serta untuk membedakan antara kelas proyek dan instansi Roblox.
Peran dari metode destroy() adalah untuk menghancurkan instansi apa pun yang dibuat oleh objek, memutus semua koneksi, dan memanggil destroy() di semua objek anak. Ini sangat penting untuk koneksi karena instansi dengan koneksi aktif tidak dibersihkan oleh pengumpul sampah Luau, bahkan jika tidak ada referensi ke instansi atau koneksi yang tersisa.
Singleton
Singleton, seperti namanya, adalah kelas di mana hanya satu objek yang dapat ada. Mereka adalah setara proyek dengan Layanan Roblox. Alih-alih menyimpan referensi ke objek singleton dan melewatkannya di dalam kode Luau, Plant memanfaatkan fakta bahwa memerlukan ModuleScript menyimpan nilai yang dikembalikan. Ini berarti bahwa memerlukan ModuleScript singleton yang sama dari tempat yang berbeda secara konsisten memberikan objek yang sama yang dikembalikan. Satu-satunya pengecualian untuk aturan ini adalah jika lingkungan yang berbeda (klien atau server) mengakses ModuleScript.
Singleton dibedakan dari kelas yang dapat diinstansiasi dengan fakta bahwa mereka tidak memiliki metode new(). Sebaliknya, objek beserta metode dan statusnya langsung dikembalikan melalui ModuleScript. Karena singleton tidak diinstansiasi, sintaks self tidak digunakan dan metode sebaliknya dipanggil dengan titik (.) daripada dengan titik dua (:).
Inferensi tipe ketat
Luau mendukung pengetikan bertahap yang berarti Anda bebas menambahkan definisi tipe opsional ke beberapa atau semua kode Anda. Dalam proyek ini, strict typechecking digunakan untuk setiap skrip. Ini adalah opsi paling sedikit yang diperbolehkan untuk alat Analisis Skrip Roblox dan karenanya kemungkinan terbesar untuk menangkap kesalahan tipe sebelum runtime.
Sintaks kelas bertipe
Pendekatan yang telah mapan untuk membuat kelas dalam Lua telah didokumentasikan dengan baik, namun tidak cocok untuk pengetikan Luau yang kuat. Dalam Luau, pendekatan termudah untuk mendapatkan tipe kelas adalah metode typeof():
type ClassType = typeof(Class.new())
Ini berhasil tetapi tidak terlalu berguna ketika kelas Anda dipicu dengan nilai yang hanya ada pada runtime, misalnya objek Player. Selain itu, asumsi yang dibuat dalam sintaks kelas Lua idiomatik adalah bahwa mendeklarasikan metode pada kelas self akan selalu merupakan instansi dari kelas tersebut; ini bukan asumsi yang dapat dibuat oleh mesin inferensi tipe.
Untuk mendukung inferensi tipe ketat, proyek Plant menggunakan solusi yang berbeda dari sintaks kelas idiomatik Lua dengan sejumlah cara, beberapa di antaranya mungkin terasa tidak intuitif:
- Definisi self digandakan, baik dalam deklarasi tipe maupun dalam konstruktor. Ini memperkenalkan beban pemeliharaan, tetapi peringatan akan ditandai jika kedua definisi tidak sinkron satu sama lain.
- Metode kelas dideklarasikan dengan titik, sehingga self dapat secara eksplisit dideklarasikan sebagai tipe ClassType. Metode masih dapat dipanggil dengan dua titik seperti yang diharapkan.
--!strict
local MyClass = {}
MyClass.__index = MyClass
export type ClassType = typeof(setmetatable(
{} :: {
property: number,
},
MyClass
))
function MyClass.new(property: number): ClassType
local self = {
property = property,
}
setmetatable(self, MyClass)
return self
end
function MyClass.addOne(self: ClassType)
self.property += 1
end
return MyClass
Mengonversi tipe setelah penjaga logis
Pada saat penulisan, tipe sebuah nilai tidak dipersempit setelah pernyataan penjaga bersyarat. Misalnya, setelah penjaga di bawah ini, tipe optionalParameter tidak dipersempit menjadi number.
--!strict
local function foo(optionalParameter: number?)
if not optionalParameter then
return
end
print(optionalParameter + 1)
end
Untuk mengatasi ini, variabel baru dibuat setelah penjaga ini dengan tipe yang secara eksplisit dikonversi.
--!strict
local function foo(optionalParameter: number?)
if not optionalParameter then
return
end
local parameter = optionalParameter :: number
print(parameter + 1)
end
Menelusuri hierarki DataModel
Dalam beberapa kasus, basis kode perlu menelusuri hierarki data model dari pohon objek yang dibuat pada runtime. Ini menghadirkan tantangan menarik untuk pemeriksaan tipe. Pada saat penulisan, tidak mungkin untuk mendefinisikan hierarki data model generik sebagai tipe. Akibatnya, ada kasus di mana satu-satunya informasi tipe yang tersedia untuk struktur model data adalah tipe instansi tingkat atas.
Salah satu pendekatan untuk tantangan ini adalah mengonversi ke any dan kemudian menyaring. Misalnya:
local function enableVendor(vendor: Model)
local zonePart: BasePart = (vendor :: any).ZonePart
end
Masalah dengan pendekatan ini adalah bahwa ini berdampak pada keterbacaan. Sebagai gantinya, proyek ini menggunakan modul generik bernama getInstance untuk menelusuri hierarki model data yang mengonversi ke any secara internal.
local function enableVendor(vendor: Model)
local zonePart: BasePart = getInstance(vendor, "ZonePart")
end
Seiring dengan evolusi pemahaman mesin tipe terhadap model data, mungkin pola seperti ini tidak lagi diperlukan.
Antarmuka pengguna
Plant mencakup berbagai antarmuka pengguna 2D yang kompleks dan sederhana. Ini termasuk item tampilan heads-up (HUD) non-interaktif seperti penghitung koin dan menu interaktif kompleks seperti toko.
Pendekatan UI
Anda dapat secara longgar membandingkan UI Roblox dengan DOM HTML, karena ini adalah hierarki objek yang menggambarkan apa yang seharusnya dilihat pengguna. Pendekatan untuk membuat dan memperbarui UI Roblox secara luas dibedakan menjadi praktik imperatif dan deklaratif.
| Pendekatan | Keuntungan dan kerugian |
|---|---|
| Imperatif | Dalam pendekatan imperatif, UI diperlakukan seperti hierarki instansi lain di Roblox. Struktur UI dibuat sebelum runtime di Studio dan ditambahkan ke model data, biasanya langsung di StarterGui. Kemudian, pada runtime, kode memanipulasi bagian tertentu dari UI untuk mencerminkan status yang dibutuhkan oleh pembuat. Pendekatan ini memiliki beberapa keuntungan. Anda dapat membuat UI dari awal di Studio dan menyimpannya di model data. Ini adalah pengalaman pengeditan yang sederhana dan visual yang dapat mempercepat pembuatan UI. Karena kode UI imperatif hanya memperhatikan apa yang perlu diubah, itu juga membuat perubahan kecil pada UI menjadi mudah diimplementasikan. Kekurangan yang mencolok adalah bahwa, karena pendekatan UI imperatif memerlukan status untuk diimplementasikan secara manual dalam bentuk transformasi, representasi status yang kompleks dapat menjadi sangat sulit ditemukan dan diperbaiki. Umum bagi kesalahan muncul ketika mengembangkan kode UI imperatif, terutama ketika status dan UI menjadi tidak sinkron karena beberapa pembaruan yang berinteraksi dalam urutan yang tidak terduga. Tantangan lain dengan pendekatan imperatif adalah bahwa lebih sulit untuk memecah UI menjadi komponen yang bermakna yang dapat dideklarasikan sekali dan digunakan kembali. Karena seluruh pohon UI dideklarasikan pada waktu edit, pola umum mungkin diulang di berbagai bagian model data. |
| Deklaratif | Dalam pendekatan deklaratif, status yang diinginkan dari instansi UI dideklarasikan secara eksplisit, dan implementasi efisien dari status ini disembunyikan oleh pustaka seperti Roact atau Fusion. Keuntungan dari pendekatan ini adalah bahwa implementasi status menjadi sepele dan Anda hanya perlu menjelaskan bagaimana UI Anda terlihat. Ini membuat mengidentifikasi dan menyelesaikan bug menjadi jauh lebih mudah. Kekurangan kuncinya adalah bahwa Anda harus mendeklarasikan seluruh pohon UI dalam kode. Pustaka seperti Roact dan Fusion memiliki sintaks yang membuat ini lebih mudah, tetapi ini masih merupakan proses yang memakan waktu dan pengalaman pengeditan yang kurang intuitif saat menyusun UI. |
Plant menggunakan pendekatan imperatif dengan pemikiran bahwa menunjukkan transformasi secara langsung memberikan gambaran yang lebih efektif tentang bagaimana UI dibuat dan dimanipulasi di Roblox. Ini tidak akan mungkin dilakukan dengan pendekatan deklaratif. Beberapa struktur dan logika UI yang berulang juga diabstraksi menjadi komponen yang dapat digunakan kembali untuk menghindari jebakan umum dalam desain UI imperatif.
Arsitektur tingkat tinggi

Lapisan dan komponen
Dalam Plant, semua struktur UI adalah baik Layer atau Component.
- Layer didefinisikan sebagai singleton pengelompok utama yang membungkus struktur UI prefabrikasi di ReplicatedStorage. Sebuah lapisan dapat berisi sejumlah komponen, atau dapat mengenkapsulasi logikanya sendiri sepenuhnya. Contoh lapisan adalah menu inventaris atau indikator jumlah koin di tampilan heads up.
- Component adalah elemen UI yang dapat digunakan kembali. Ketika objek komponen baru diinstansiasi, ia mengkloning template prefabrikasi dari ReplicatedStorage. Komponen mungkin sendiri mengandung komponen lain. Contoh komponen adalah kelas tombol umum atau konsep daftar item.
Penanganan tampilan
Masalah manajemen UI yang umum adalah penanganan tampilan. Proyek ini memiliki berbagai menu dan item HUD, beberapa di antaranya mendengarkan input pengguna, dan manajemen yang cermat tentang kapan mereka terlihat atau diaktifkan diperlukan.
Plant mendekati masalah ini dengan sistem UIHandler yang mengelola kapan lapisan UI harus atau tidak harus terlihat. Semua lapisan UI dalam pengalaman dikategorikan sebagai HUD atau Menu dan visibilitasnya dikelola oleh aturan berikut:
- Status diaktifkan dari lapisan Menu dan HUD dapat diubah.
- Lapisan HUD yang diaktifkan hanya ditampilkan jika tidak ada lapisan Menu yang diaktifkan.
- Lapisan Menu yang diaktifkan disimpan dalam tumpukan, dan hanya satu lapisan Menu yang terlihat pada satu waktu. Ketika lapisan Menu diaktifkan, itu ditempatkan di bagian depan tumpukan dan ditampilkan. Ketika lapisan Menu dinonaktifkan, itu dihapus dari tumpukan dan lapisan Menu yang diaktifkan berikutnya dalam antrean ditampilkan.
Pendekatan ini intuitif karena memungkinkan menu dinavigasi dengan riwayat. Jika satu menu dibuka dari menu lain, menutup menu baru akan menampilkan menu lama lagi.
Singleton lapisan UI mendaftar diri mereka dengan UIHandler dan diberikan dengan sinyal yang mengeluarkan saat visibilitasnya harus berubah.
Bacaan lebih lanjut
Dari gambaran menyeluruh tentang proyek Plant, Anda mungkin ingin menjelajahi panduan berikut yang membahas lebih dalam konsep dan topik terkait.
- Model Klien-Server — Tinjauan model klien-server di Roblox.
- Acara dan Callback Jarak Jauh — Semua tentang acara jaringan jarak jauh dan callback untuk komunikasi di seluruh batas klien-server.
- UI — Rincian tentang objek dan desain antarmuka pengguna di Roblox.