Drawing and animation (ပုံဆွဲခြင်းနဲ့ animation)

IA (Internal Assessment: IB သင်တန်းမှာ ကျောင်းသားတစ်ယောက်ချင်း ကိုယ်တိုင် တည်ဆောက်ရပြီး ကျောင်းက ဆရာက အမှတ်ပေးတဲ့ အပိုင်း) အတွက် tool တွေပါ၊ စာမေးပွဲ ခေါင်းစဉ် မဟုတ်ပါဘူး။ ဒါတွေက ပုံဆွဲတဲ့ tool တွေ၊ simulation (တကယ့် ဖြစ်စဉ်ကို ကွန်ပျူတာနဲ့ အတုလုပ်ပြခြင်း) တွေနဲ့ game အသေးလေးတွေအတွက် JavaFX (Java နဲ့ desktop app တည်ဆောက်တဲ့ library) ပုံစံတွေပါ။ Form (ဒေတာ ဖြည့်ရတဲ့ အကွက်တွေ ပါတဲ့ အပိုင်း) နဲ့ table (ဇယား) တွေကို အဓိက သုံးတဲ့ IA အများစုမှာ ဒါတွေ မလိုပါဘူး။ ဒီထဲက ဘာကိုမှ စာမေးပွဲမှာ မမေးပါဘူး၊ ပြီးတော့ IA က ကိုယ်တိုင် လုပ်ထားတဲ့ အလုပ် ဖြစ်ရပါမယ်။

Canvas ဆိုတာ ပုံသဏ္ဌာန်တွေ၊ မျဉ်းတွေ၊ စာသားတွေနဲ့ ပုံတွေကို တိုက်ရိုက် ဆွဲလို့ရတဲ့ ဗလာ စတုဂံ တစ်ခုပါ။ ဒီစာမျက်နှာမှာ canvas ပေါ်မှာ ဆွဲတာ၊ mouse ကို တုံ့ပြန်တာနဲ့ ချောမွေ့တဲ့ animation (ပုံတွေကို ဆက်တိုက် ပြောင်းဆွဲပြီး လှုပ်ရှားနေသလို ပြခြင်း) အတွက် loop (ထပ်ခါထပ်ခါ run တဲ့ code အပိုင်း) တစ်ခု run တာတို့ကို လေ့လာပါမယ်။ ကိုယ့် IA က ပုံဆွဲတဲ့ tool၊ simulation ဒါမှမဟုတ် game ပုံစံ app ဖြစ်မှသာ ဒါတွေကို သုံးပါ။

ဒီ cookbook ကို ပထမဆုံး ဖတ်တာလား။ Start here: how every recipe fits together (ဒီမှာ စပါ: recipe တိုင်း ဘယ်လို ချိတ်ဆက်ထားလဲ) ကို အရင် ဖတ်ပါ။ အဲဒီ စာမျက်နှာက ကိုယ် ပြင်ရမယ့် နေရာ နှစ်ခုကို ရှင်းပြထားပြီး recipe (ချက်ချင်း ယူသုံးလို့ရတဲ့ code နမူနာ) တိုင်း အားကိုးရတဲ့ fx:id (screen ပေါ်က button၊ text box စတာတွေကို code ထဲက variable နဲ့ ချိတ်တဲ့ နာမည်) ချိတ်ဆက်မှုကိုလည်း ရှင်းပြထားပါတယ်။


Canvas ပေါ်မှာ ပုံသဏ္ဌာန်တွေနဲ့ မျဉ်းတွေ ဆွဲခြင်း

ဒါက ဘာလုပ်ပေးလဲ: Canvas ရဲ့ GraphicsContext ကို သုံးပြီး Canvas ပေါ်မှာ ပုံသဏ္ဌာန်တွေ၊ မျဉ်းတွေနဲ့ စာသားတွေကို ဆွဲပေးပါတယ်။ GraphicsContext ဆိုတာ ပုံဆွဲတဲ့ ညွှန်ကြားချက်တွေကို ကိုင်ထားတဲ့ object (class တစ်ခုကနေ ဖန်တီးထားတဲ့ instance) ပါ။

ကိုယ့် IA မှာ ဘယ်အခါ သုံးမလဲ: game အတွက် ကစားကွက်၊ ရိုးရှင်းတဲ့ diagram (အချက်အလက်ကို ပုံနဲ့ ရှင်းပြတဲ့ ပုံ)၊ ကိုယ်တိုင် အပြည့်အဝ ထိန်းချုပ်ချင်တဲ့ chart (ဒေတာကို ပုံနဲ့ ပြတဲ့ ဂရပ်) ဒါမှမဟုတ် စိတ်ကြိုက် ပုံဆွဲလို့ရတဲ့ မျက်နှာပြင် ဘယ်ဟာမဆို။

SceneBuilder ထဲမှာ (layout): SceneBuilder (FXML layout ကို ပုံနဲ့ မြင်ရင်း ပြင်တဲ့ editor) ထဲက layout (control တွေကို screen ပေါ်မှာ စီစဉ်ထားပုံ) မှာ Canvas တစ်ခု ထည့်ပြီး canvas ဆိုတဲ့ fx:id ပေးပါ၊ ပြီးရင် သူ့ width (အကျယ်) နဲ့ height (အမြင့်) ကို သတ်မှတ်ပါ။

<Canvas fx:id="canvas" width="300" height="200" />

Controller (user လုပ်တာတွေကို ကိုင်တွယ်ပြီး Model နဲ့ screen တွေကို ချိတ်ပေးတဲ့ class) ထဲမှာ (.java file):

Import (တခြားနေရာက code ကို ယူသုံးမယ်လို့ ကြေညာခြင်း) line များ:

import javafx.fxml.FXML;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.paint.Color;

Controller ထဲမှာ:

@FXML private Canvas canvas;

@FXML
public void initialize() {
    GraphicsContext gc = canvas.getGraphicsContext2D();

    // your IA logic here: what to draw
    gc.setFill(Color.CORNFLOWERBLUE);
    gc.fillRect(20, 20, 100, 60);          // x, y, width, height

    gc.setStroke(Color.DARKRED);
    gc.strokeLine(20, 100, 200, 100);      // from (x1, y1) to (x2, y2)

    gc.fillOval(140, 20, 50, 50);          // x, y, width, height of the bounding box

    gc.strokeText("Hello", 20, 150);       // text at (x, y)
}

ကိုယ်ပိုင် logic (ဘာလုပ်မလဲ ဆုံးဖြတ်တဲ့ code) ကို ဘယ်မှာ ထည့်ရမလဲ: // your IA logic here (ကိုယ့် IA ရဲ့ logic ကို ဒီမှာ ထည့်ပါ) လို့ မှတ်ထားတဲ့ ပုံဆွဲတဲ့ method (သီးခြား အလုပ်တစ်ခု လုပ်ပေးတဲ့ code အပိုင်း) ခေါ်မှုတွေပါ၊ အဲဒါတွေက ကိုယ့်ရဲ့ ပုံပါ။ GraphicsContext ကို ယူတာက ပြန်သုံးလို့ရတဲ့ wiring (control တွေနဲ့ code ကို ချိတ်ဆက်ပေးတဲ့ အသင့် code) ပါ။

ကိုယ့် project နဲ့ ကိုက်အောင် ပြင်ပါ:

  • ကိုယ့် ပုံနဲ့ ကိုက်အောင် အရောင်တွေ၊ coordinate (တည်နေရာ ပြတဲ့ ကိန်း) တွေနဲ့ အရွယ်အစားတွေကို ပြောင်းပါ။
  • setFill ပြီးရင် fill... method တစ်ခု ခေါ်တာက အပြည့် ဆိုးထားတဲ့ ပုံသဏ္ဌာန်ကို ဆွဲပါတယ်၊ setStroke ပြီးရင် stroke... method တစ်ခု ခေါ်တာက အနားသတ် မျဉ်းကို ဆွဲပါတယ်။

သတိထားရမယ့် အချက်များ:

  • fx:id canvas က @FXML field (FXML ထဲက control နဲ့ ချိတ်ထားတဲ့ controller ထဲက variable) နာမည်နဲ့ အတိအကျ ကိုက်ရပါမယ်။ မကိုက်ရင် screen က load မဖြစ်ပါဘူး။
  • Coordinate တွေက ဘယ်ဘက် အပေါ်ထောင့်က (0, 0) ကနေ စပါတယ်၊ ပြီးတော့ y က အပေါ်ကို မဟုတ်ဘဲ အောက်ကို တိုးပါတယ်။ လူအများစု တစ်ခါတော့ ဒီမှာ မှားဖူးကြပါတယ်။
  • Canvas က “ဆေးသုတ်ပြီး မေ့လိုက်” ပုံစံပါ: ကိုယ် ဆွဲနေတာက object တွေ မဟုတ်ဘဲ pixel (ပုံထဲက အစက် တစ်စက်) တွေပါ။ ပုံသဏ္ဌာန် တစ်ခုကို ဆွဲပြီးတာနဲ့ အဲဒါက သီးခြား ခွဲခြားသိလို့ရတဲ့ အရာ မဟုတ်တော့လို့ ရွှေ့ချင်ရင် ဒါမှမဟုတ် ဖယ်ချင်ရင် အဲဒီ နေရာကို ရှင်းပြီး ပြန်ဆွဲရပါတယ်။ အောက်က animation recipe က ဒီအချက်ပေါ်မှာ မူတည်ပါတယ်။

တွဲသုံးလို့ရတဲ့ recipe များ: User နှိပ်တဲ့ ဒါမှမဟုတ် ဆွဲရွှေ့တဲ့ နေရာမှာ ပုံဆွဲခြင်း၊ ချောမွေ့တဲ့ animation ဒါမှမဟုတ် game loop ကို run ခြင်း။


User နှိပ်တဲ့ ဒါမှမဟုတ် ဆွဲရွှေ့တဲ့ နေရာမှာ ပုံဆွဲခြင်း

ဒါက ဘာလုပ်ပေးလဲ: canvas ပေါ်က mouse ရဲ့ တည်နေရာကို ဖတ်ပြီး အဲဒီနေရာမှာ ဆွဲပေးပါတယ်၊ ဒါကြောင့် user က နှိပ်ပြီး ဒါမှမဟုတ် ဆွဲရွှေ့ပြီး အမှတ်အသားတွေ လုပ်လို့ ရပါတယ်။

ကိုယ့် IA မှာ ဘယ်အခါ သုံးမလဲ: ပုံဆွဲ ဒါမှမဟုတ် ပုံကြမ်းဆွဲ tool တစ်ခု၊ ကစားကွက်ပေါ်မှာ ကစားရုပ်တွေ ချတာ ဒါမှမဟုတ် diagram ပေါ်မှာ user ကို အမှတ်တွေ မှတ်ခွင့်ပေးတာ။

SceneBuilder ထဲမှာ (layout): Canvas (fx:id canvas) ကို ထည့်ပြီး onMousePressed ရော onMouseDragged ကိုပါ handler (button နှိပ်တာလို event ဖြစ်တဲ့အခါ run တဲ့ code) တစ်ခုတည်း ဖြစ်တဲ့ handleDraw လို့ သတ်မှတ်ပါ။

<Canvas fx:id="canvas" width="300" height="200"
        onMousePressed="#handleDraw" onMouseDragged="#handleDraw" />

Controller ထဲမှာ (.java file):

Import line များ:

import javafx.fxml.FXML;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.input.MouseEvent;
import javafx.scene.paint.Color;

Controller ထဲမှာ (handler တစ်ခုတည်းက နှိပ်တာရော ဆွဲရွှေ့တာကိုပါ ကိုင်တွယ်ပါတယ်):

@FXML private Canvas canvas;

@FXML
private void handleDraw(MouseEvent event) {
    GraphicsContext gc = canvas.getGraphicsContext2D();
    gc.setFill(Color.BLACK);
    // your IA logic here: what to draw at the mouse position
    // event.getX() and event.getY() are the mouse position on the canvas.
    gc.fillOval(event.getX() - 5, event.getY() - 5, 10, 10);
}

ကိုယ်ပိုင် logic ကို ဘယ်မှာ ထည့်ရမလဲ: // your IA logic here လို့ မှတ်ထားတဲ့ mouse တည်နေရာမှာ ဆွဲမယ့် အရာပါ။ Event (ဖြစ်ရပ်) ထဲကနေ mouse တည်နေရာကို ဖတ်တာက ပြန်သုံးလို့ရတဲ့ wiring ပါ။

ကိုယ့် project နဲ့ ကိုက်အောင် ပြင်ပါ:

  • Mouse တည်နေရာမှာ ဆွဲမယ့် အရာကို ပြောင်းပါ: အစက် တစ်စက်၊ နောက်ဆုံး အမှတ်ကနေ ဆွဲတဲ့ မျဉ်း တစ်ကြောင်း၊ game ကစားရုပ် တစ်ခု။
  • onMousePressed က နှိပ်တိုင်း တစ်ကြိမ် အလုပ်လုပ်ပါတယ်၊ onMouseDragged က mouse button ကို ဖိထားရင်း mouse ရွှေ့နေသရွေ့ ထပ်ခါထပ်ခါ အလုပ်လုပ်ပါတယ်။ နှစ်ခုလုံး သုံးရင် မပြတ်မတောက် ဆွဲပေးတဲ့ စုတ်တံ ရပါတယ်။
  • 5 (10 ကျယ်တဲ့ oval (ဘဲဥပုံ) ရဲ့ တစ်ဝက်) ကို နုတ်လိုက်တာက အမှတ်အသားကို cursor (screen ပေါ်က mouse ညွှန်တံ) ရဲ့ အလယ် တည့်တည့်မှာ ရောက်အောင် လုပ်ပေးပါတယ်။

သတိထားရမယ့် အချက်များ:

  • fx:id canvas က @FXML field နာမည်နဲ့ ကိုက်ရပါမယ်။ မကိုက်ရင် screen က load ဖြစ်ပေမဲ့ field က null (ဘာ object ကိုမှ မညွှန်းတဲ့ တန်ဖိုး) ဖြစ်နေပြီး user က click လုပ်တဲ့အခါ ဆွဲတဲ့ code မှာ error ဖြစ်ပါလိမ့်မယ်။ handleDraw ကတော့ MouseEvent parameter (method ကို ပေးလိုက်တဲ့ တန်ဖိုးကို လက်ခံတဲ့ variable) နဲ့ controller ထဲမှာ ရှိရပါမယ်၊ မရှိရင် screen က load မဖြစ်ပါဘူး။
  • event.getX() နဲ့ event.getY() ကို canvas ရဲ့ ဘယ်ဘက် အပေါ်ထောင့်ကနေ တိုင်းပါတယ်၊ canvas ပေါ်မှာ ဆွဲတဲ့အခါ လိုချင်တာလည်း ဒါပါပဲ။
  • Mouse button ကို လွှတ်လိုက်တဲ့အခါ သီးခြား လုပ်ဆောင်ချက် တစ်ခု (ဥပမာ မျဉ်း တစ်ကြောင်းကို အဆုံးသတ်တာ) လိုရင် တခြား method တစ်ခုကို ညွှန်းတဲ့ onMouseReleased ကို ထည့်ပါ။

တွဲသုံးလို့ရတဲ့ recipe များ: Canvas ပေါ်မှာ ပုံသဏ္ဌာန်တွေနဲ့ မျဉ်းတွေ ဆွဲခြင်း၊ React to the Enter key (Enter key ကို တုံ့ပြန်ခြင်း)။


ချောမွေ့တဲ့ animation ဒါမှမဟုတ် game loop ကို run ခြင်း

ဒါက ဘာလုပ်ပေးလဲ: AnimationTimer ကို သုံးပြီး frame (screen ပေါ်မှာ တစ်ကြိမ် ပြတဲ့ ပုံ တစ်ပုံ) တိုင်းမှာ (တစ်စက္ကန့်ကို 60 ကြိမ်လောက်) code အပိုင်း တစ်ခုကို run ပါတယ်။ ချောမွေ့တဲ့ လှုပ်ရှားမှုကို ပေးတာ ဒါပါပဲ။

ကိုယ့် IA မှာ ဘယ်အခါ သုံးမလဲ: sprite (game ထဲမှာ ရွှေ့ပြတဲ့ ပုံ အသေး) တစ်ခုကို ရွှေ့တာ၊ physics (ရူပဗေဒ) simulation ဒါမှမဟုတ် particle (အမှုန်လေးတွေ) simulation၊ ဒါမှမဟုတ် အဆက်မပြတ် update လုပ်ပြီး ပြန်ဆွဲနေတဲ့ ဘယ် game loop (game ကို update လုပ်ပြီး ပြန်ဆွဲတာကို ဆက်တိုက် လုပ်နေတဲ့ loop) မဆို။

SceneBuilder ထဲမှာ (layout): ပုံဆွဲမယ့် Canvas (fx:id canvas) ကို ထည့်ပါ။ Loop ကိုတော့ code ထဲမှာ ဖန်တီးပါတယ်။

<Canvas fx:id="canvas" width="300" height="200" />

Controller ထဲမှာ (.java file):

Import line များ:

import javafx.animation.AnimationTimer;
import javafx.fxml.FXML;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.paint.Color;

Controller ထဲမှာ:

@FXML private Canvas canvas;

private double ballX = 0;
private AnimationTimer loop;                   // a field, so other methods can stop and start it

@FXML
public void initialize() {
    GraphicsContext gc = canvas.getGraphicsContext2D();

    loop = new AnimationTimer() {
        @Override
        public void handle(long now) {
            // 1. wipe the canvas so the last frame does not leave a trail
            gc.clearRect(0, 0, canvas.getWidth(), canvas.getHeight());

            // your IA logic here: update positions and draw this frame
            gc.setFill(Color.TOMATO);
            gc.fillOval(ballX, 80, 30, 30);

            ballX += 2;
            if (ballX > canvas.getWidth()) {
                ballX = 0;                         // wrap back to the left
            }
        }
    };
    loop.start();
}

ကိုယ်ပိုင် logic ကို ဘယ်မှာ ထည့်ရမလဲ: // your IA logic here လို့ မှတ်ထားတဲ့ frame တစ်ခုစီရဲ့ update လုပ်တာနဲ့ ဆွဲတာပါ။ Loop၊ canvas ကို ရှင်းတာနဲ့ start/stop တွေက ပြန်သုံးလို့ရတဲ့ wiring ပါ။

ကိုယ့် project နဲ့ ကိုက်အောင် ပြင်ပါ:

  • Frame တစ်ခုစီမှာ run ရမယ့် ကိုယ့် logic ကို handle ထဲမှာ ထည့်ပါ: အရာတွေကို ရွှေ့တာ၊ တစ်ခုနဲ့ တစ်ခု ဝင်တိုက်မိသလား စစ်တာ၊ ပြန်ဆွဲတာ။
  • ပြောင်းနေတဲ့ တန်ဖိုးတွေ (တည်နေရာ၊ အမြန်နှုန်း) ကို frame တစ်ခုကနေ နောက်တစ်ခုဆီ ဆက်ရှိနေအောင် field (object ထဲက အချက်အလက် အကွက်) တွေအဖြစ် ထားပါ။
  • Animation ကို ခဏ ရပ်ဖို့ loop.stop() ကို ခေါ်ပါ၊ ပြန်ဆက်ဖို့ loop.start() ကို ခေါ်ပါ။ loop က field ဖြစ်လို့ ဘယ် method ကမဆို ဒီလို ခေါ်လို့ရပါတယ်၊ ဥပမာ Pause button ရဲ့ handler ကနေ ခေါ်လို့ရပါတယ်။

သတိထားရမယ့် အချက်များ:

  • fx:id canvas က @FXML field နာမည်နဲ့ အတိအကျ ကိုက်ရပါမယ်။ မကိုက်ရင် screen က load မဖြစ်ပါဘူး။
  • Frame တစ်ခုစီရဲ့ အစမှာ canvas ကို ရှင်းပါ။ မရှင်းရင် ဆွဲထားသမျှ ကျန်နေပြီး လှုပ်ရှားမှု အစား အစင်းကြောင်းတွေ ရပါလိမ့်မယ်။
  • handle က တစ်စက္ကန့်ကို 60 ကြိမ်လောက် run လို့ သူ့ထဲက အလုပ်ကို ပေါ့ပေါ့ပါးပါး ထားပါ။ အဲဒီမှာ အလုပ် များများ လုပ်ရင် app တစ်ခုလုံး ထစ်ထစ် ဖြစ်သွားပါတယ်။
  • AnimationTimer က Timeline နဲ့ ဆင်တူပေမဲ့ ပိုလေးလံတဲ့ class ပါ။ တစ်စက္ကန့်ချင်း နောက်ပြန် ရေတွက်တာလို ပုံသေ အချိန်ခြားပြီး လုပ်ရမယ့် အလုပ်အတွက် Timeline ကို သုံးပြီး frame တိုင်းမှာ ချောမွေ့တဲ့ လှုပ်ရှားမှု လိုတဲ့အခါ AnimationTimer ကို သုံးပါ။
  • new AnimationTimer() { ... } ပုံစံက override (parent ရဲ့ method ကို child က အစားထိုး ရေး) လုပ်ထားတဲ့ handle method ပါတဲ့ anonymous class (နာမည် မပေးဘဲ code ထဲမှာ တန်းရေးတဲ့ class) တစ်ခုပါ။ Override လုပ်တာက ပုံမှန် OOP (object တွေကို အခြေခံပြီး program ရေးနည်း) ပါ၊ code ထဲမှာ တန်းရေးတဲ့ class ကတော့ timer ကို သူ့ရဲ့ method တစ်ခုတည်းကို ပေးဖို့ အဆင်ပြေတဲ့ နည်း တစ်ခုပါ။

တွဲသုံးလို့ရတဲ့ recipe များ: Do something on a repeating beat (သတ်မှတ်ချိန် ခြားပြီး ထပ်ခါထပ်ခါ လုပ်ခြင်း)၊ Keep the UI responsive during slow work (ကြာတဲ့ အလုပ် လုပ်နေစဉ် app ရဲ့ screen ကို တုံ့ပြန်နေအောင် ထားခြင်း)၊ Play a sound (အသံ ဖွင့်ခြင်း)။