Game Series: Bikin Game Pertama Kamu dengan BabylonJS dan Blazor (C#)

Hi Rekan Devs,

Ternyata akhir pekan sudah berakhir, sebelum memulai kerja atau kuliah, yuk kita coba bikin game sederhana dengan BabylonJS dan Blazor.

Hmm,, kenapa babylonJS ? Ya karena sekarang lagi trend Metaverse jadi kalian harus tetap kekinian dengan belajar programming 3D biar bisa bikin game / aplikasi XR. BabylonJS adalah 3d engine yang dibuat dengan typescript yang jalan di browser dengan memanfaatkan WebGL, dan saat ini sudah mulai support untuk XR Device seperti Oculus dan Hololens juga. referensi: WebVR Experience Helper | Babylon.js Documentation (babylonjs.com)

Kenapa Blazor ? Nah dengan blazor kamu bisa bikin web app yang bisa jalan sebagai web assembly app yang dikoding dengan C#, nah kenikmatannya dobel yah haha. ref: Blazor | Build client web apps with C# | .NET (microsoft.com)

Untuk setup environment rekan-rekan coba baca artikel sebelumnya disini:

VR Series: Membangun Simulasi 3D dengan Babylon JS dan Blazor – Makers.ID

Nah, cuma koding di index.razornya di ubah menjadi seperti dibawah ini:

using Microsoft.AspNetCore.Components.Web;
using BABYLON;
using EventHorizon.Blazor.Interop.Callbacks;
using babylon_wasm.Client;

namespace PongGame.Client.Pages
{

    public partial class Index : IDisposable
    {
        //babylonjs engine
        private Engine _engine;
        private DebugLayerScene _scene;
        protected override void OnAfterRender(bool firstRender)
        {
            if (firstRender)
            {

            }
        }

        protected override async Task OnAfterRenderAsync(bool firstRender)
        {
            if (firstRender)
            {
                //start scene
                await CreateSceneAsync();
            }
        }

        public void Dispose()
        {
            _engine?.dispose();
        }

        Game game;
        public async Task CreateSceneAsync()
        {
            //bikin canvas dari div
            var canvas = Canvas.GetElementById("game-window");
            //init engine babylon
            var engine = new Engine(canvas, true);
            // extend layer scene untuk debug
            _scene = new DebugLayerScene(engine);
            //kasih light ke scene
            HemisphericLight light = new BABYLON.HemisphericLight("light", new BABYLON.Vector3(1, 1, 0), _scene);
            light.intensity = 1;
            //init game kita
            game = new Game(_scene);
            //jalankan fungsi render ke scene
            engine.runRenderLoop(new ActionCallback(() => Task.Run(() => _scene.render(true, false))));

            _engine = engine;
        }

        //kalau kamu mijit keyboard
        protected void HandleKeyDown(KeyboardEventArgs args)
        {
            Console.WriteLine(args.Key);
            switch (args.Key.ToLower())
            {
                //teken w tuk geser pad ke atas
                case "w":
                    game?.MovePadUp();
                    break;
                //teken s untuk geser pad ke bawah
                case "s":
                    game?.MovePadDown();
                    break;

                default:
                    //do nothing
                    break;
            }
            if (args.ShiftKey && args.CtrlKey && args.AltKey && args.Key.ToLower() == "i")
            {
                if (_scene.debugLayer.isVisible())
                {
                    Console.WriteLine("Hello");
                    _scene.debugLayer.hide();
                }
                else
                {
                    _scene.debugLayer.show();
                }
            }
        }
    }

    public class Game
    {
        public enum Players
        {
            Human, Com
        }
        Scene scene;
        const decimal GameWidth = 20;
        const decimal GameHeight = 10;
        const decimal BallDiameter = 1;
        const decimal PadWidth = 3;
        const decimal PadDepth = 0.25m;
        const decimal PadToBorderDist = 1.5m;
        const decimal MovePadDist = 0.5m;
        const decimal BorderDepth = 0.25m;
        const decimal GapSize = 0.75m;

        //vars
        Mesh Ball;
        Mesh MyPad;
        Mesh ComPad;

        //score labels
        BABYLON.GUI.TextBlock PlayerScoreTxt;
        BABYLON.GUI.TextBlock ComScoreTxt;
        BABYLON.GUI.TextBlock StatusTxt;
        //posisi awal game
        public decimal StartX { set; get; } = 0;
        public decimal StartY { set; get; } = 0;
        public Game(Scene scene, decimal X = 0, decimal Y = 0)
        {
            this.scene = scene;
            this.StartX = X;
            this.StartY = Y;
            InitScene();
        }
        //geser pad ke atas
        public void MovePadUp(Players player = Players.Human)
        {
            if (player == Players.Human)
            {
                if (MyPad.position.z < GameHeight - (PadWidth / 2))
                    MyPad.position.z += MovePadDist;
            }
            else
            {
                if (ComPad.position.z < GameHeight - (PadWidth / 2))
                    ComPad.position.z += MovePadDist;
            }
        }
        //geser pad ke bawah
        public void MovePadDown(Players player = Players.Human)
        {
            if (player == Players.Human)
            {
                if (MyPad.position.z - (PadWidth / 2) > 0)
                    MyPad.position.z -= MovePadDist;
            }
            else
            {
                if (ComPad.position.z - (PadWidth / 2) > 0)
                    ComPad.position.z -= MovePadDist;
            }
        }

        void InitScene()
        {
            //border game
            var border_up = BABYLON.MeshBuilder.CreateBox("border-up", new { height = 1, width = GameWidth, depth = BorderDepth }, scene);
            var border_right = BABYLON.MeshBuilder.CreateBox("border-right", new { height = 1, width = GameHeight, depth = BorderDepth }, scene);
            var border_left = BABYLON.MeshBuilder.CreateBox("border-left", new { height = 1, width = GameHeight, depth = BorderDepth }, scene);
            var border_down = BABYLON.MeshBuilder.CreateBox("border-down", new { height = 1, width = GameWidth, depth = BorderDepth }, scene);
            border_up.position.x = StartX + (0.5m * GameWidth);
            border_up.position.z = StartY;

            border_down.position.x = StartX + (0.5m * GameWidth);
            border_down.position.z = StartY + GameHeight;

            border_left.position.x = StartX;
            border_left.position.z = StartY + (0.5m * GameHeight);
            border_left.rotate(BABYLON.Axis.Y, BABYLON.Tools.ToRadians(90));

            border_right.position.x = StartX + (GameWidth);
            border_right.position.z = StartY + (0.5m * GameHeight);
            border_right.rotate(BABYLON.Axis.Y, BABYLON.Tools.ToRadians(90));
            //ball
            Ball = BABYLON.MeshBuilder.CreateSphere("ball", new { diameter = BallDiameter }, scene);
            Ball.position = new Vector3(GameWidth / 2, 0, GameHeight / 2);
            var ballMat = new BABYLON.StandardMaterial("ballmat", scene);
            ballMat.diffuseColor = new BABYLON.Color3(0, 0.5m, 0.7m);
            Ball.material = ballMat;

            //pads
            MyPad = BABYLON.MeshBuilder.CreateBox("player-pad", new { height = 1, width = PadWidth, depth = PadDepth }, scene);
            ComPad = BABYLON.MeshBuilder.CreateBox("com-pad", new { height = 1, width = PadWidth, depth = PadDepth }, scene);

            MyPad.position.x = StartX + PadToBorderDist;
            MyPad.position.z = StartY + (0.5m * GameHeight);
            MyPad.rotate(BABYLON.Axis.Y, BABYLON.Tools.ToRadians(90));
            var playerpadmat = new BABYLON.StandardMaterial("playerpadmat", scene);
            playerpadmat.diffuseColor = new BABYLON.Color3(0.85m, 0, 0);
            MyPad.material = playerpadmat;

            ComPad.position.x = StartX + (GameWidth) - PadToBorderDist;
            ComPad.position.z = StartY + (0.5m * GameHeight);
            ComPad.rotate(BABYLON.Axis.Y, BABYLON.Tools.ToRadians(90));
            var compadmat = new BABYLON.StandardMaterial("compadmat", scene);
            compadmat.diffuseColor = new BABYLON.Color3(0, 0.85m, 0);
            ComPad.material = compadmat;

            //cam
            #region arc rotate cam
            var camera = new BABYLON.ArcRotateCamera("camera", (decimal)(-System.Math.PI / 2), (decimal)(System.Math.PI / 2.5), 30, new BABYLON.Vector3(StartX + (0.5m * GameWidth), 0, (0.5m * GameHeight)), scene);
            camera.upperBetaLimit = (decimal)(System.Math.PI / 2.2);
            camera.attachControl(true);
            #endregion

            //score
            var adt = BABYLON.GUI.AdvancedDynamicTexture.CreateFullscreenUI("UI", true, scene: scene);

            var panel = new BABYLON.GUI.StackPanel("stack1");
            panel.width = "220px";
            panel.top = "-25px";
            panel.horizontalAlignment = BABYLON.GUI.Control.HORIZONTAL_ALIGNMENT_RIGHT;
            panel.verticalAlignment = BABYLON.GUI.Control.VERTICAL_ALIGNMENT_BOTTOM;
            adt.addControl(panel);

            PlayerScoreTxt = new BABYLON.GUI.TextBlock("score1");
            PlayerScoreTxt.text = "Player: ";
            PlayerScoreTxt.height = "30px";
            PlayerScoreTxt.color = "white";
            panel.addControl(PlayerScoreTxt);

            ComScoreTxt = new BABYLON.GUI.TextBlock("score2");
            ComScoreTxt.text = "Com: ";
            ComScoreTxt.height = "30px";
            ComScoreTxt.color = "white";
            panel.addControl(ComScoreTxt);

            StatusTxt = new BABYLON.GUI.TextBlock("status");
            StatusTxt.text = "-";
            StatusTxt.height = "50px";
            StatusTxt.color = "red";
            panel.addControl(StatusTxt);

            //button tuk mulai game
            var btn = BABYLON.GUI.Button.CreateSimpleButton("btnStart", "Start Game");
            btn.horizontalAlignment = BABYLON.GUI.Control.HORIZONTAL_ALIGNMENT_LEFT;
            btn.verticalAlignment = BABYLON.GUI.Control.VERTICAL_ALIGNMENT_BOTTOM;
            btn.textBlock.color = "white";
            btn.width = "200px";
            btn.height = "50px";
            btn.paddingLeft = "10px";
            btn.paddingBottom = "10px";
            btn.onPointerClickObservable.add((i, e) =>
            {
                StartGame();
                return Task.CompletedTask;
            });
            adt.addControl(btn);

        }
        void StartGame()
        {
            //klu mulai game, cek dulu sebelumnya uda mulai atau belum
            if (IsRunning && cts != null)
            {
                cts.Cancel();
                Thread.Sleep(200);
            }
            cts = new CancellationTokenSource();
            //reset posisi bola
            Ball.position = new Vector3(GameWidth / 2, 0, GameHeight / 2);
            gameThread = Task.Run(async () => Loop(cts.Token));
            gameThread.Start();
        }
        Task gameThread;
        public bool IsRunning { get; set; } = false;
        CancellationTokenSource cts;
        //game looping
        async Task Loop(CancellationToken GameToken)
        {

            decimal BallX = Ball.position.x, BallY = Ball.position.z, BallDX = 0.2m, BallDY = 0.4m;
            int ScoreHuman = 0, ScoreCom = 0;
            IsRunning = true;

            while (true)
            {
                //kluar kalau token d cancel
                if (GameToken.IsCancellationRequested)
                {

                    break;
                }

                BallX += BallDX;
                BallY += BallDY;
                //cek kalau bola nubruk tembok kiri
                if (BallX <= GapSize && BallDX < 0)
                {
                    //win
                    //play sound
                    ScoreCom++;
                    BallDX *= -1;
                }
                //cek klu bola nubruk pad player
                else if (BallX >= MyPad.position.x && BallX <= MyPad.position.x + GapSize && BallDX < 0 && BallY >= MyPad.position.z - (PadWidth / 2) && BallY <= MyPad.position.z + (PadWidth / 2))
                {
                    //pantul
                    BallDX *= -1;
                }
                //cek kalau bola nubruk tembok kanan
                if (BallX >= GameWidth - GapSize && BallDX > 0)
                {
                    //win
                    //play sound
                    ScoreHuman++;
                    BallDX *= -1;
                }
                //cek kalau bola nubruk pad com
                else if (BallX + GapSize >= ComPad.position.x && BallX <= ComPad.position.x + PadDepth && BallDX > 0 && BallY >= ComPad.position.z - (PadWidth / 2) && BallY <= ComPad.position.z + (PadWidth / 2))
                {
                    //pantul
                    BallDX *= -1;
                }

                //cek klu bola nubruk langit2 dan bawah
                if (BallY <= GapSize || BallY >= GameHeight - GapSize)
                {
                    BallDY *= -1;

                }

                //move ball
                Ball.position.x = BallX;
                Ball.position.z = BallY;

                // Computer
                if (BallY > ComPad.position.z)
                    MovePadUp(Players.Com);
                else
                if (BallY < ComPad.position.z)
                    MovePadDown(Players.Com);

                // Score
                PlayerScoreTxt.text = $"Player: {ScoreHuman}";
                ComScoreTxt.text = $"Com: {ScoreCom}";

                if (ScoreHuman >= 5)
                {
                    StatusTxt.text = "You Win!";
                    break;
                }

                if (ScoreCom >= 5)
                {
                    StatusTxt.text = "You Lose!";
                    break;
                }
                //delay - kasih kesempatan tuk render UI blazor
                await Task.Delay(50);
            }
            IsRunning = false;
        }
    }
}

Nah kalau dijalanin error, mending tarik source lengkapnya dari Gravicode/Babylon.PongGame: This is experimental project to build simple game using BabylonJS and Blazor WASM (github.com)

Karena di code-nya uda dikasih komentar, jadi ga usa dijelasin lagi disini, uda pada pinter khan..

Met Explorasi,

Salam Dev 😉

Loading

You May Also Like