CLICK HERE TO READ THE ENGLISH VERSION

Membuat Game Sederhana dengan Blazor Web Assembly

Mengembangkan game “Monkey Counting” dengan Blazor Web Assembly .NET 8

Juldhais Hengkyawan

--

Saya membuat game sederhana “Monkey Counting” untuk anak saya yang berusia dua tahun:

Website tersebut dikembangan dengan Blazor Web Assembly dan di-host Azure Static Apps. Ini adalah bagian dari perjalanan saya belajar Blazor dengan cara praktek langsung.

Artikel kali ini akan menjelaskan bagaimana cara membuat game “Monkey Counting” dari dasar.

Mari kita mulai.

Membuat Project Blazor Web Assembly Baru

Buka Visual Studio 2022, kemudian pilih project template yang bernama Blazor Web Assembly Standalone App.

Atur nama project menjadi BlazorCountingGame atau dengan nama apapun.

Atur framework menjadi .NET 8.0, authentication type menjadi None, dan pilih Configure for HTTPS.

Click tombol [Create] untuk membuat project baru. Struktur project yang terbentuk akan terlihat seperti ini:

Menambahkan Bootstrap CSS dari CDN

Selanjutkan kita akan menggunakan bootstrap CSS untuk styling. Caranya buka file index.html yang berada di dalam folder wwwroot, kemudian tambahkan link bootstrap CSS di bawah ini ke dalam tag head:

<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" 
integrity="sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN" crossorigin="anonymous">

Saya juga mengubah isi file index.html agar lebih sederhana. Isi lengkap dari file tersebut adalah seperti berikut ini:

<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Monkey Count - juldhais.net</title>
<base href="/" />
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css"
integrity="sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN" crossorigin="anonymous">
<link rel="stylesheet" href="css/app.css" />
</head>

<body>
<div id="app">
Loading...
</div>

<script src="_framework/blazor.webassembly.js"></script>
</body>

</html>

Random Number untuk Jumlah Monyet

Buka file Home.razor yang terdapat pada folder Pages. Di sinilah kita akan menulis code untuk game yang akan kita buat:

@page "/"

<PageTitle>Monkey Count</PageTitle>

<div class="mx-4 my-4">
<div class="display-3 text-center">How many monkeys are there?</div>

<div>
<div class="row row-cols-3">
@for (int i = 0; i < count; i++)
{
<div class="col">🙉</div>
}
</div>
</div>
</div>

@code {
int count;

protected override void OnInitialized()
{
NextQuestion();
}

void NextQuestion()
{
// generate random number of monkeys
count = Random.Shared.Next(1, 10);
}
}

@page Directive

@page "/"
Directive ini membuat component menjadi halaman yang dapat diakses melalui URL root (“/”). Ini adalah routing endpoint dari aplikasi.

PageTitle Component

<PageTitle>Monkey Count</PageTitle>
Mengatur judul dari halaman web yang akan ditampilkan pada browser tab.

HTML Content

Element <div class="mx-4 my-4"> berfungsi sebagai main container dengan margin yang diatur dengan Bootstrap class (mx-4 untuk horizontal margin danmy-4 untuk vertical margin).

Struktur div digunakan untuk membuat grid layout (menggunakan sistem grid Bootstrap dengan class row row-cols-3) yang akan menampilkan emoji monyet. Jumlah emoji yang ditampilkan dihasilkan secara dinamis berdasarkan nilai dari variable count.

@code Block

@code block berisi code C# code yang menangani interactivy pada halaman:

  • count adalah variable yang menyimpan nilai banyaknya emoji monyet yang akan ditampilkan.
  • OnInitialized adalah lifecycle method yang di dalamnya dipanggil methodNextQuestion ketika component pertama kali diinisialisasi.
  • NextQuestion adalah method yang menghasilkan random number antara 1 sampai 9 dan menyimpannya ke dalam variable count.

Membuat dan Mengacak Pilihan Jawaban

Pemain harus memilih jumlah monyet yang benar dari tiga pilihan jawaban.

Kita perlu mengubah section code menjadi sebagai berikut:

@code {
int count;
List<int> choices = [];

protected override void OnInitialized()
{
NextQuestion();
}

void NextQuestion()
{
// generate random number
count = Random.Shared.Next(1, 10);

choices.Clear();

// add count to choices
choices.Add(count);

// add 2 more unique random number to choices
while (choices.Count < 3)
{
int choice = Random.Shared.Next(1, 9);

if (!choices.Contains(choice))
{
choices.Add(choice);
}
}

// shuffle the choices using Fisher-Yates algorithm
var n = choices.Count;
while (n > 1)
{
n--;
int k = Random.Shared.Next(n + 1);
int value = choices[k];
choices[k] = choices[n];
choices[n] = value;
}
}
}

Variable choices adalah list of integer yang digunakan untuk menyimpan pilihan jawaban.

Perubahan pada method NextQuestion adalah sebagai berikut:

  • Fungsichoices.Clear() menghapus semua pilihan dari pertanyaan sebelumnya.
  • Fungsi choices.Add(count) memasukkan nilai variablecount (jawaban yang benar) ke dalam list choices.
  • Looping while digunakan untuk memasukkan nilai random 1 sampai 9 ke dalam listchoices hingga terdapat tiga jawaban. Pilihan dipastikan harus unique.
  • Terakhir, kita mengimplementasikan algoritma Fisher-Yates untuk mengacak isi dari list choices.

Berikutnya kita akan menampilan isi dari list choices:

<div class="mx-4 my-4">

...

<div class="row">
@foreach (var choice in choices)
{
<div class="col-4 text-center border py-3">
@choice
</div>
}
</div>
</div>

Event Handler Ketika Jawaban Dipilih

Kita perlu membuat method CheckAnswer untuk meng-handle event ketika pemain memilih jawaban. Jika jawaban yang dipilih benar, maka lanjut ke pertanyaan berikutnya.

@code {

...

void CheckAnswer(int choice)
{
if (choice == count)
{
NextQuestion();
}
}
}

Kita juga harus mengatur event callback @onclick untuk memanggil method CheckAnswer ketika pilihan di-click:

<div class="row">
@foreach (var choice in choices)
{
<div class="col-4 text-center border py-3" @onclick="(() => CheckAnswer(choice))">
@choice
</div>
}
</div>

Menambahkan Custom CSS dan Animation

Kita akan menambahkan custom CSS untuk membuat si monyet terlihat lebih besar, dan juga memainkan animasi ketika jawaban yang benar atau salah dipilih.

Custom CSS

Buka file app.css yang terdapat pada folder wwwroot/css, kemudian ganti isinya seperti berikut ini:

.monkey {
font-size: 60px;
text-align: center;
}

.choice {
font-size: 60px;
cursor: pointer;
}

@keyframes shake {

0%, 100% {
transform: translateX(0);
}

10%, 30%, 50%, 70%, 90% {
transform: translateX(-10px);
}

20%, 40%, 60%, 80% {
transform: translateX(10px);
}
}

.shake {
animation: shake 0.82s cubic-bezier(.36, .07, .19, .97) both;
color: darkred;
}

@keyframes jump {

0%, 100% {
transform: translateY(0);
}

10%, 30%, 50%, 70%, 90% {
transform: translateY(-10px);
}

20%, 40%, 60%, 80% {
transform: translateY(0);
}
}

.jump {
animation: jump 1s ease;
color: green;
}

Sekarang kita kembali ke file Home.razor, kemudian tambahkan css class .monkey dan .choice ke element yang sesuai:

<div style="height:330px">
<div class="row row-cols-3">
@for (int i = 0; i < count; i++)
{
<div class="col monkey">🙉</div>
}
</div>
</div>

<div class="row">
@foreach (var choice in choices)
{
<div class="col-4 text-center border py-3 choice" @onclick="(() => CheckAnswer(choice))">
@choice
</div>
}
</div>

Animation

Animasi “jump” akan dimainkan ketika pemain memilih jawaban yang benar. Jika jawabannya salah, maka animasi “shake” yang akan dimainkan.

Kita harus secara dinamis menambahkan kelas .shake atau .jump tergantung pada jawaban yang dipilih pemain. Dalam JavaScript, kita dapat dengan mudah melakukan hal tersebut dengan menggunakan kombinasi dari getElementById dan classList.add.

Namun dalam Blazor, mengakses DOM element secara langsung bukanlah standard approach. Blazor menggunakan component based architecture, di mana kita mengontrol UI element mengunakan state dan properties.

Kita perlu menambahkan dua buah variable animationClass dan selectedChoice agar animasi dapat dijalankan dengan sesuai.

@code {
...
string animationClass = "";
int? selectedChoice = null;

...

}

Kita juga perlu menambahkan method TriggerAnimation dan GetAnimationClass:

@code {

...

async Task TriggerAnimation(int choice)
{
// prevent overlapping animation
if (selectedChoice != null) return;

selectedChoice = choice;

animationClass = selectedChoice == count ? "jump" : "shake";

await Task.Delay(1500);

animationClass = "";

selectedChoice = null;
}

string GetAnimationClass(int choice)
{
return selectedChoice == choice ? animationClass : "";
}
}

Kita harus mengubah method CheckAnswer untuk memanggil method TriggerAnimation method dan juga memanggil StateHasChanged untuk memberi tahu component kalau terjadi perubahan pada state-nya:

async void CheckAnswer(int choice)
{
await TriggerAnimation(choice);

if (choice == count)
{
NextQuestion();
}

StateHasChanged();
}

Agar animasi hanya dimainkan pada jawaban yang dipilih, kita perlu menggunakan method GetAnimationClass(choice) di dalam looping@foreach yang me-render pilihan jawaban:

<div class="row">
@foreach (var choice in choices)
{
<div class="col-4 text-center border py-3 choice" @onclick="(() => CheckAnswer(choice))">
<div class="@GetAnimationClass(choice)">
@choice
</div>
</div>
}
</div>

Kita juga dapat menjalankan animasi pada gambar monyet dengan menambahkan @animationClass ke monkey emoji:

<div style="height:330px">
<div class="row row-cols-3">
@for (int i = 0; i < count; i++)
{
<div class="col monkey @animationClass">🙉</div>
}
</div>
</div>

--

--