iT邦幫忙

2025 iThome 鐵人賽

DAY 5
1

昨天,我們成功讓第一個 Sprite 動了起來。有了控制單一物件動態的經驗後,今天我們將面臨一個更實際的問題:當一個遊戲角色由多個物件組成時,要怎麼一起控制它們?

想像一下,一個遊戲角色不僅有主角的 Sprite,還有血條、ID 名稱等。如果每次移動都要分別去設定每個物件的位置,那程式碼將會變得非常混亂。

解決這個問題的關鍵就是 Container,你可以把它想像成一個透明的容器,用來將多個遊戲物件裝在一起。當你移動或旋轉這個容器時,裡面所有的東西都會跟著一起移動。

▸ 開始動手

首先,將你的 app.ts 程式碼清空,並貼上以下程式碼:

import pixi = CG.Pixi.pixi;

// 設定移動速度
let speed = 3;

async function start() {

    // 載入資源(記得將資源別名修改成你自己專案的資源別名喔!)
    await pixi.assets.add("ironman2025_cook.圖片.女巫").load();

	// 初始化 Pixi
    await pixi.initialize({ stageWidth: 960, stageHeight: 540 });

    // 創建一個容器物件,用來裝所有東西,並設定位置在畫面中央
    const container = new PIXI.Container();
    container.position.set(pixi.stageWidth * 0.5, pixi.stageHeight * 0.5);

    // 創建女巫 Sprite 物件並設定錨點為中心
    const witch = pixi.assets.createSprite("ironman2025_cook.圖片.女巫");
    witch.anchor.set(0.5);

    // 創建一個代表血條的紅色方塊
    const healthBar = new PIXI.Graphics()
        .rect(-30, -3, 60, 6)
        .fill(0xFF0000)
        .stroke({
            color: 0xFFFFFF,
            width: 1
        } as PIXI.StrokeStyle);
    // 將血條的位置設定在女巫上方
    healthBar.y = -witch.height * 0.5 - 10;

    // 將 Sprite 和血條放入容器中
    container.addChild(witch);
    container.addChild(healthBar);

    // 將容器加入舞台
    pixi.root.addChild(container);

    // 添加一個循環更新函數
    CG.Base2.addUpdateFunction(() => {

        // 更新容器的位置
        container.x += speed;

        // 判斷容器是否碰到舞台邊界,並反轉方向
        if (container.x > pixi.stageWidth || container.x < 0) {
            speed *= -1;
            // 改變女巫的 x 軸縮放來讓它轉向
            witch.scale.x = speed / Math.abs(speed);
        }
        
    });
}

start();

貼上程式碼並點擊「試玩遊戲」,你應該會看到一個帶著紅色血條的女巫在畫面上來回移動,並且在碰到邊界時會自動轉向。而且我們只控制了 Container 的位置,但裡面的女巫和血條卻一起跟著動了!

女巫水平移動 GIF

▸ Container(容器)

Container 是 PixiJS 中一種基本的顯示物件,它的主要功能是作為其他顯示物件的群組容器

Container 本身是不可見的,沒有顏色也沒有大小。它就像一個無形的盒子,當你把 SpriteGraphics 或其他 Container 放進這個盒子裡,這些子物件的位置、縮放和旋轉都會相對於這個盒子來計算。這意味著,當你移動這個盒子時,裡面的所有東西都會跟著一起移動!

我最喜歡舉這個例子了,容器與子物件的關係,就像是餐桌與桌上的餐盤、食物的關係。

  • 當你拿起餐盤上的食物,食物會慢慢靠近你的嘴巴,然後被你吃掉,其他東西都還保留在原地,因為沒人去動它們。
  • 當你拿你餐盤,除了餐盤會移動,餐盤上的食物也會跟著移動,萬一你不小心手滑了,食物就會跟著餐盤一起回歸大地。
  • 當一群人一起抬起餐桌挪動位置,餐桌上的所有東西就會跟著一起被移動,要是突然其中一個人不開心翻桌了,那......,我只能幫你的肚子默哀了。

PixiJS 餐桌物件圖層關係圖

這套邏輯也完全適用於程式碼的世界,我們只需要控制這個「餐桌」,就能輕鬆管理所有「餐盤」和「食物」了。

在最新的 PixiJS v8 版本中,Container 已經完全取代了 DisplayObject,成為了所有顯示物件的基類,DisplayObject 也已經被淘汰。

▸ 拆解程式碼

這段程式碼的核心,在於我們如何將多個物件整合成一個群組來管理。

1. 創建容器與子物件

const container = new PIXI.Container();
container.position.set(pixi.stageWidth * 0.5, pixi.stageHeight * 0.5);

我們首先創建了一個新的 Container 物件,並設定它的初始位置在畫面的正中央。

接著,我們創建了兩個子物件:

  • witchSprite 物件,正是我們前兩天重點介紹的女巫,只是名字從 sprite 改成了 witch
  • healthBarGraphics 物件,用來繪製血條。Graphics 是一個非常實用的類別,它讓你可以用程式碼動態地繪製簡單的形狀,像是矩形(rect)、圓形(circle)或線條(line)等等。目前暫時先大概了解即可,明天我們再來深入介紹它。

2. 管理物件與加入舞台

// 將 Sprite 和血條放入容器中
container.addChild(witch);
container.addChild(healthBar);

// 將容器加入舞台
pixi.root.addChild(container);

這兩組程式碼是 Container 的精髓所在。

  • container.addChild():這行程式碼將 witchhealthBar 這兩個子物件放進了 container 這個容器裡。一旦被放進去,它們的位置就會相對於 container 的原點來計算。
  • pixi.root.addChild(container):這行程式碼則是將整個容器 (container) 加入到我們的主舞台 (pixi.root)。

這是一個非常重要的概念:只有被加到舞台上的物件,才會被顯示出來。而當我們把一個容器加到舞台上時,容器裡的所有子物件也會跟著被顯示。

你知道嗎?其實 pixi.root 也一個 Container

3. 動畫與邏輯

// 更新容器的位置
container.x += speed;

// 判斷容器是否碰到舞台邊界,並反轉方向
if (container.x > pixi.stageWidth || container.x < 0) {
    speed *= -1;
    // 改變女巫的 x 軸縮放來讓它轉向
    witch.scale.x = speed / Math.abs(speed);
}

這裡的程式碼在每次畫面更新時執行。

  • container.x += speed:我們只改變了容器的 x 軸位置。但因為女巫和血條都在容器裡,它們會跟著容器一起移動,這就是 Container 的核心用途。
  • if 判斷式:這段邏輯跟昨天一樣,用來判斷容器是否碰到左右邊界,並讓它反彈。但這次我們多加了一行 witch.scale.x = speed / Math.abs(speed);,這行程式碼會根據 speed 的正負值,將女巫的 x 軸縮放設定為 1-1,從而實現自動轉向的效果。

還記得昨天留下的謎團嗎?scale.x = -1,沒錯,他會直接讓顯示物件轉向,x 軸為負數時,顯示物件會水平翻轉,y 軸為負數時,顯示物件會垂直翻轉。不知道這個問題有沒有讓你半夜苦惱的睡不著覺呢?

▸更進階的動畫

在結束之前,讓我們再看看一個情況,將下方新的程式碼添加到原本的 addUpdateFunction 中,其他程式碼保持不變。

CG.Base2.addUpdateFunction(() => {

    // ... 其餘保留 ...

    // 簡單的動畫,讓女巫上下輕微浮動
    const timing = Date.now() % 2000 / 2000;
    const scale = Math.sin(Math.PI * timing * 2);
    witch.y = 5 * scale;

});

這段程式碼會讓女巫以正弦波的方式上下輕微浮動,製造一種飄動的感覺。

  • Date.now() 會回傳目前的毫秒數。
  • % 2000 / 2000 會將這個毫秒數轉換成一個介於 0 ~ 1 之間的數字,代表每 2 秒循環一次。
  • Math.sin(Math.PI * timing * 2) 則將這個介於 0 ~ 1 的數字轉換成一個在 -1 ~ 1 之間擺動的數字。
  • 最後,我們將 5 乘上這個擺動的數字,並設定給 witch.y,這樣女巫就會在垂直方向上輕微浮動了。

如果你覺得這段數學運算太複雜也沒關係,這只是提供給對數學有興趣的朋友參考。對於遊戲開發來說,你只需要知道,透過這段程式碼,我們成功讓子物件在跟隨父物件移動的同時,還能擁有自己的獨立動畫

女巫水平漂浮移動 GIF

點我查看範例程式碼

▸ 總結

今天的重點內容其實只有 Container,作為顯示物件的容器:

  • 你可以將各式各樣的顯示物件加入到容器中。
  • 當你移動、縮放或旋轉一個容器時,裡面的所有子物件都會跟著一起改變。
  • 子物件的大部分屬性,都是相對於容器計算的,如位置、縮放、旋轉角度、透明度等。

明天,我們將深入介紹 Graphics 這個類別,介紹如何用程式碼畫出各種形狀。


上一篇
Day 04:讓你的 Sprite 動起來 - 位置、縮放與旋轉
下一篇
Day 06:繪製你的第一個形狀 - Graphics
系列文
用 PixiJS 寫遊戲!告別繁瑣設定,在 Code.Gamelet 打造你的第一個遊戲6
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言