Tanaman adalah game referensi di mana pemain menanam dan menyiram biji, sehingga mereka dapat memanen dan menjual tanaman yang dihasilkan.

Proyek ini berfokus pada kasus penggunaan umum yang mungkin Anda temui saat mengembangkan game di Roblox. Di mana pun memungkinkan, Anda akan menemukan catatan tentang tradeoff, kompromi, dan alasan dari berbagai pilihan implementasi, sehingga Anda dapat membuat keputusan terbaik untuk game Anda sendiri.
Dapatkan file
- Navigasikan ke halaman game Tanaman.
- Klik tombol ⋯ dan Edit di Studio.
Kasus penggunaan
Tanaman 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 menyelesaikan seperangkat masalah yang lebih sempit yang berlaku untuk banyak game, termasuk:
- Kustomisasi area di tempat yang terkait dengan pemain
- Mengelola kecepatan gerak karakter pemain
- Membuat objek yang mengikuti karakter
- Mendeteksi bagian mana dari dunia yang sedang dijelajahi karakter
Perhatikan bahwa ada beberapa kasus penggunaan dalam game ini yang terlalu kecil, terlalu niche, atau tidak menunjukkan solusi untuk tantangan desain yang menarik; ini tidak dicakup.
Struktur proyek
Keputusan pertama saat membuat game adalah memutuskan bagaimana cara menyusun proyek, yang terutama mencakup di mana menempatkan instance tertentu dalam model data dan bagaimana mengorganisir serta menyusun titik masuk untuk kode klien dan server.
Model data
Tabel berikut menjelaskan layanan kontainer mana dalam model data yang digunakan untuk menempatkan instance.
| Layanan | Jenis instance |
|---|---|
| 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 instance ini secara dinamis saat runtime, jadi tidak masalah jika dibiarkan di sini. Juga terdapat Folder kosong, di mana model pertanian pemain akan ditambahkan saat runtime. |
| Lighting | Efek atmosfer dan pencahayaan. |
| ReplicatedFirst | Berisi subset terkecil dari instance yang diperlukan untuk menampilkan layar pemuatan dan menginisialisasi game. Semakin banyak instance yang ditempatkan di ReplicatedFirst, semakin lama waktu tunggu untuk mereplikasi sebelum kode di ReplicatedFirst dapat dijalankan.
|
| ReplicatedStorage | Berfungsi sebagai kontainer penyimpanan untuk semua instance yang memerlukan akses di klien dan server.
|
| ServerScriptService | Berisi Script yang berfungsi sebagai titik masuk untuk semua kode sisi server dalam proyek. |
| ServerStorage | Berfungsi sebagai kontainer penyimpanan untuk semua instance yang tidak perlu direplikasi ke klien.
|
| SoundService | Berisi objek Sound yang digunakan untuk efek suara dalam game. 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 dan dapat diimpor di seluruh basis kode. ModuleScripts dapat digunakan kembali tetapi 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 game, menciptakan beberapa titik masuk.
Untuk mikrogame Tanaman, pendekatan yang berbeda diterapkan melalui satu LocalScript yang merupakan titik masuk untuk semua kode klien, dan satu Script yang merupakan 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 menggambarkan 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 melewatkan objek dengan referensi antara sistem.
Arsitektur sistem tingkat tinggi
Sistem tingkat atas dalam proyek dijelaskan di bawah ini. Beberapa sistem ini secara substansial lebih kompleks daripada yang lain, dan dalam banyak kasus fungsionalitas mereka diabstraksikan melalui hierarki kelas lain.

Setiap sistem ini adalah "singleton," di mana ia adalah kelas yang tidak dapat diinstansiasi yang diinisialisasi oleh skrip start klien atau server yang relevan. Anda dapat membaca lebih lanjut tentang pola singleton di bagian selanjutnya dari 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 game 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 dikaitkan dengan pemain yang bertahan antara sesi penyimpanan. Sistem ini menyediakan replikasi dari klien ke server dan seperangkat API yang dapat digunakan untuk menanyakan data dan berlangganan pada 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 melalui event PlayerDataClient.updated.
PlayerDataServer.setValue(player, "coins", 5)Tentu saja, ini hanya berguna untuk replikasi dari 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 khusus 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 instance itu sendiri.
Ini sangat berguna untuk instance yang dibuat saat runtime, karena atribut yang diatur pada instance baru sebelum diparent ke model data akan mereplikasi secara atomik dengan instance itu sendiri. Ini menghindari kebutuhan untuk menulis kode untuk "menunggu" data tambahan direplikasi melalui RemoteEvent atau StringValue.
Anda juga dapat langsung membaca atribut dari model data, baik dari klien atau server, dengan metode GetAttribute() dan berlangganan pada perubahan dengan metode GetAttributeChangedSignal(). Dalam proyek Tanaman, pendekatan ini digunakan untuk, antara lain, mereplikasi status saat ini dari tanaman ke klien.
Replikasi melalui tag
CollectionService memungkinkan Anda menerapkan tag string ke Instance. Ini berguna untuk mengkategorikan instance dan mereplikasi kategorisasi itu ke klien.
Sebagai contoh, tag CanPlant diterapkan di server untuk menandakan kepada klien bahwa pot tertentu dapat menerima tanaman.
Mengirim pesan langsung melalui modul jaringan
Untuk situasi di mana tidak ada opsi sebelumnya yang berlaku, Anda dapat menggunakan panggilan jaringan khusus melalui modul Jaringan. Ini adalah satu-satunya opsi dalam proyek yang memungkinkan komunikasi dari klien ke server dan oleh karena itu paling berguna untuk mentransmisikan permintaan klien dan menerima respons dari server.
Tanaman menggunakan panggilan jaringan langsung untuk berbagai permintaan klien, termasuk:
- Menyiram tanaman
- Menanam biji
- Membeli item
Kekurangan dari pendekatan ini adalah bahwa setiap pesan individu memerlukan beberapa konfigurasi khusus yang dapat meningkatkan kompleksitas proyek, meskipun ini telah dihindari di mana pun memungkinkan, terutama untuk komunikasi dari server ke klien.
Kelas dan singleton
Kelas dalam proyek Tanaman, seperti instance di Roblox, dapat dibuat dan dihancurkan. Sintaks kelasnya terinspirasi oleh pendekatan idiomatik Lua untuk pemrograman berorientasi objek dengan sejumlah perubahan untuk mendukung dukungan pemeriksaan tipe 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 instance 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 membuat objek Beam antara dua objek Attachment yang diberikan dan menjaga agar lampiran tersebut terorientasi sehingga sinar selalu menghadap ke atas. Instance ini dapat diklon dari versi prefabrikasi di ReplicatedStorage atau diteruskan ke new() sebagai argumen dan disimpan di dalam objek di bawah self.
Instance yang sesuai
Seperti yang disebutkan di atas, banyak kelas dalam proyek ini memiliki representasi model data, sebuah instance yang sesuai dengan kelas dan dimanipulasi olehnya.
Alih-alih membuat instance 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 instance ini dan membuatnya dari awal dalam fungsi new() kelas, melakukannya akan membuat pengeditan objek menjadi sangat merepotkan dan membuatnya lebih sulit bagi pembaca untuk memahami. Selain itu, mengkloning instance umumnya merupakan operasi yang lebih cepat daripada membuat instance baru dan menyesuaikan propertinya saat runtime.
Komposisi
Meskipun pewarisan dimungkinkan di Luau menggunakan metatables, proyek ini memilih untuk membiarkan kelas memperluas satu sama lain melalui komposisi. Saat menggabungkan kelas melalui komposisi, objek "anak" diinstansiasi dalam metode new() kelas dan disertakan sebagai anggota di bawah self.
Sebagai contoh dari ini dalam tindakan, lihat kelas CloseButton yang membungkus kelas Button.
Pembersihan
Mirip 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 seluruh metode basis kode, serta untuk membedakan antara kelas proyek dan instance Roblox.
Peran metode destroy() adalah untuk menghancurkan instance apa pun yang dibuat oleh objek, memutuskan koneksi apa pun, dan memanggil destroy() pada objek anak mana pun. Ini sangat penting untuk koneksi karena instance dengan koneksi aktif tidak dibersihkan oleh pengumpul sampah Luau, bahkan jika tidak ada referensi ke instance atau koneksi ke instance 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 meneruskannya di dalam kode Luau, Tanaman 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 dikembalikan langsung melalui ModuleScript. Karena singleton tidak diinstansiasi, sintaks self tidak digunakan dan metode dipanggil dengan titik (.) daripada titik dua (:).
Pemeriksaan tipe ketat
Luau mendukung pengetikan bertahap yang berarti Anda bebas menambahkan definisi tipe opsional ke beberapa atau semua kode Anda. Dalam proyek ini, pemeriksaan tipe strict digunakan untuk setiap skrip. Ini adalah opsi paling ketat untuk alat Analisis Skrip Roblox dan dengan demikian paling mungkin menangkap kesalahan tipe sebelum runtime.
Sintaks kelas bertipe
Pendekatan yang sudah mapan untuk membuat kelas di Lua didokumentasikan dengan baik, namun tidak cocok untuk pengetikan Luau yang kuat. Di Luau, pendekatan termudah untuk mendapatkan tipe kelas adalah metode typeof():
type ClassType = typeof(Class.new())Ini berfungsi tetapi tidak terlalu berguna ketika kelas Anda diinisiasi dengan nilai yang hanya ada saat runtime, misalnya objek Player. Selain itu, asumsi yang dibuat dalam sintaks kelas Lua idiomatik adalah bahwa mendeklarasikan metode pada kelas self akan selalu menjadi instance dari kelas itu; ini bukan asumsi yang dapat dibuat oleh mesin inferensi tipe.
Untuk mendukung inferensi tipe ketat, proyek Tanaman menggunakan solusi yang berbeda dari sintaks kelas Lua idiomatik dalam sejumlah cara, beberapa di antaranya mungkin terasa tidak intuitif:
- Definisi self diduplikasi, 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 titik dua 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 MyClassCast tipe setelah penjaga logis
Pada saat penulisan, tipe nilai tidak dipersempit setelah pernyataan penjaga kondisional. Sebagai contoh, 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)
endUntuk mengatasi ini, variabel baru dibuat setelah penjaga ini dengan tipe yang secara eksplisit di-cast.
--!strict
local function foo(optionalParameter: number?)
if not optionalParameter then
return
end
local parameter = optionalParameter :: number
print(parameter + 1)
endMenelusuri hierarki DataModel
Dalam beberapa kasus, basis kode perlu menelusuri hierarki model data dari pohon objek yang dibuat saat runtime. Ini menghadirkan tantangan menarik untuk pemeriksaan tipe. Pada saat penulisan, tidak mungkin untuk mendefinisikan hierarki model data generik sebagai tipe. Akibatnya, ada kasus di mana satu-satunya informasi tipe yang tersedia untuk struktur model data adalah tipe dari instance tingkat atas.
Salah satu pendekatan untuk tantangan ini adalah melakukan cast ke any dan kemudian memperbaiki. Sebagai contoh:
local function enableVendor(vendor: Model)
local zonePart: BasePart = (vendor :: any).ZonePart
endMasalah dengan pendekatan ini adalah bahwa ini mempengaruhi keterbacaan. Sebagai gantinya, proyek ini menggunakan modul generik bernama getInstance untuk menelusuri hierarki model data yang melakukan cast ke any secara internal.
local function enableVendor(vendor: Model)
local zonePart: BasePart = getInstance(vendor, "ZonePart")
endSeiring dengan berkembangnya pemahaman mesin tipe tentang model data, mungkin pola seperti ini tidak lagi diperlukan.
Antarmuka pengguna
Tanaman mencakup berbagai antarmuka pengguna 2D yang kompleks dan sederhana. Ini termasuk item tampilan kepala non-interaktif (HUD) seperti penghitung koin dan menu interaktif kompleks seperti toko.
Pendekatan UI
Anda dapat membandingkan UI Roblox UI dengan DOM HTML, karena ini adalah hierarki objek yang menggambarkan apa yang seharusnya dilihat pengguna. Pendekatan untuk membuat dan memperbarui UI Roblox secara luas dibagi menjadi praktik imperatif dan deklaratif.
| Pendekatan | Keuntungan dan kerugian |
|---|---|
| Imperatif | Dalam pendekatan imperatif, UI diperlakukan seperti hierarki instance lainnya di Roblox. Struktur UI dibuat sebelum runtime di Studio dan ditambahkan ke model data, biasanya langsung di StarterGui. Kemudian, saat runtime, kode memanipulasi bagian tertentu dari UI untuk mencerminkan status yang diinginkan 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, ini juga membuat perubahan UI sederhana mudah diimplementasikan. Salah satu kerugian 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. Kesalahan sering muncul saat mengembangkan kode UI imperatif, terutama ketika status dan UI menjadi tidak sinkron karena beberapa pembaruan 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 beberapa bagian model data. |
| Deklaratif | Dalam pendekatan deklaratif, status yang diinginkan dari instance UI dideklarasikan secara eksplisit, dan implementasi efisien dari status ini diabstraksikan oleh pustaka seperti Roact atau Fusion. Keuntungan dari pendekatan ini adalah implementasi status menjadi sepele dan Anda hanya perlu menggambarkan bagaimana Anda ingin UI Anda terlihat. Ini membuat mengidentifikasi dan menyelesaikan bug menjadi jauh lebih mudah. Kerugian utama adalah harus mendeklarasikan seluruh pohon UI dalam kode. Pustaka seperti Roact dan Fusion memiliki sintaks untuk mempermudah ini, tetapi tetap merupakan proses yang memakan waktu dan pengalaman pengeditan yang kurang intuitif saat menyusun UI. |
Tanaman menggunakan pendekatan imperatif dengan anggapan 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 diulang juga diabstraksikan menjadi komponen yang dapat digunakan kembali untuk menghindari jebakan umum dalam desain UI imperatif.
Arsitektur tingkat tinggi

Lapisan dan komponen
Dalam Tanaman, semua struktur UI adalah baik Layer atau Component.
- Layer didefinisikan sebagai singleton pengelompokan tingkat atas 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 kepala.
- Component adalah elemen UI yang dapat digunakan kembali. Ketika objek komponen baru diinstansiasi, ia mengkloning template prefabrikasi dari ReplicatedStorage. Komponen dapat berisi komponen lain. Contoh komponen adalah kelas tombol generik 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 hati-hati tentang kapan mereka terlihat atau diaktifkan diperlukan.
Tanaman mendekati masalah ini dengan sistem UIHandler yang mengelola kapan lapisan UI harus atau tidak harus terlihat. Semua lapisan UI dalam game 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, ia dimasukkan ke depan tumpukan dan ditampilkan. Ketika lapisan Menu dinonaktifkan, ia 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 sinyal yang dipicu ketika visibilitasnya harus berubah.
Bacaan lebih lanjut
Dari tinjauan menyeluruh tentang proyek Tanaman, Anda mungkin ingin menjelajahi panduan berikut yang lebih mendalam tentang konsep dan topik terkait.
- Model Klien-Server — Tinjauan tentang model klien-server di Roblox.
- Acara Jarak Jauh dan Callback — Semua tentang acara jaringan jarak jauh dan callback untuk komunikasi di seluruh batas klien-server.
- UI — Rincian tentang objek antarmuka pengguna dan desain di Roblox.