[Day2] Vite 出小蜜蜂~动画 Animation!

Day2

Animation 动画

动画在游戏中扮演非常重要的角色,
当绘制的角色在萤幕上动起来时,就像是角色活起来一样。
卡比接下来想要跟大家分享这份喜悦!

分析

Crab

Crab 在游戏里面在每次移动时都会把手举起来,感觉像在跟玩家打招呼呢!
所以我们需要再画一张把手举起来的图,并透过切换两张图达到打招呼的效果。

这类透过切换图还达成效果的动画称作 定格动画 (Frame Animation)
在很多领域都会应用的到喔!

画图

首先我们要画 Crab 举手的图形

-- src/characters/Crab.ts

const image2 = [
  [0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0],
  [1, 0, 0, 1, 0, 0, 0, 1, 0, 0, 1],
  [1, 0, 1, 1, 1, 1, 1, 1, 1, 0, 1],
  [1, 1, 1, 0, 1, 1, 1, 0, 1, 1, 1],
  [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
  [0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0],
  [0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0],
  [0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0],
];

稍微改一下我们的 Crab 函式,

export default function Crab() {
  const graphics = new Graphics();

+  const image = image2;

  for (let y = 0; y < image.length; y++) {
    for (let x = 0; x < image[y].length; x++) {
      if (image[y][x] === 0) continue;

      graphics.beginFill(0xffffff);

      graphics.drawRect(x, y, 1, 1);

      graphics.endFill();
    }
  }

  return graphics;
}

这时画面上应该长这样

Crab greeting

换图

当然,程序执行时,我们不可能透过手动更改程序码下去做更换图片,
所以我们需要先撰写好 什麽时机点要做什麽事 ,像这样的程序流程。

卡比接下来要示范,更换图形的方式。

以下方式只是众多换图方式的其中一种,大家可以尝试出不同的方式

认识 RequestAnimationFrame 与 Ticker

现代动画大部分都是以一秒刷新60个画面的方式来播放,浏览器也是。

而卡比想在每次画面刷新前都去执行某段程序码,
这个在做动画中非常重要的函式,叫做 requestAnimationFrame
透过这个函式,我们可以在画面更新之前执行某些运算,
来改变每次画面刷新的结果。

但是 requestAnimationFrame 是比较底层的 API,
实务上可能会需要更多的功能跟资料,例如. 距离上次刷新过了多久、能不能暂停 ...etc

而卡比接下来会使用的是 pixi.js 里面的 Ticker
他已经帮我们准备好了一些常用的功能。

实作

pixi.jsApplication 预设会给一个 Ticker
所以我们只需要把 src/main.ts 加工一下,

-- src/main.ts

const app = new Application({
  width: 20,
  height: 20,
  resolution: 10,
});

document.querySelector("#app")?.append(app.view);

app.stage.addChild(Crab());

+ app.ticker.add(() => {

+ });

你可以试试看在刚刚加入的 arrow function 里面执行 console.log
体验一秒执行60次的感觉。

重构

接下来卡比要在每次刷新时,
重新绘制一个 Crab 并重新新增到画面上。

-- src/characters/Crab.ts

export default function Crab() {
  return () => {
    const graphics = new Graphics();

    const image = image2;

    for (let y = 0; y < image.length; y++) {
      for (let x = 0; x < image[y].length; x++) {
        if (image[y][x] === 0) continue;

        graphics.beginFill(0xffffff);

        graphics.drawRect(x, y, 1, 1);

        graphics.endFill();
      }
    }

    return graphics;
  };
}

上面的写法叫做 Higher Order Function
就是 函式 可以回传出另一个 函式

然後在 src/main.ts 做出以下更动,

const app = new Application({
  width: 20,
  height: 20,
  resolution: 10,
});

document.querySelector("#app")?.append(app.view);

- app.stage.addChild(Crab());
+ const update = Crab();

app.ticker.add(() => {
+  app.stage.removeChildren();

+  app.stage.addChild(update());
});

这样就可以在每次画面刷新前,将画面重置,重新运算产生结果。

然後,回到 src/characters/Crab.ts

-- src/characters/Crab.ts

export default function Crab() {
  let current = 0;
  const images = [image, image2];

  return () => {
    const image = images[current % images.length];
    current += 1;

    const graphics = new Graphics();

    for (let y = 0; y < image.length; y++) {
      for (let x = 0; x < image[y].length; x++) {
        if (image[y][x] === 0) continue;

        graphics.beginFill(0xffffff);

        graphics.drawRect(x, y, 1, 1);

        graphics.endFill();
      }
    }

    return graphics;
  };
}

这样画面上的 Crab 就会挥手了!但是...
好像挥的太快了,手会不会断掉。

Throttle

因为现在每一次画面刷新都会执行运算,
实际上我们只需要一段时间执行一次而已,而其他的时间不执行运算,
Throttle 这个时候就派上用场了。

首先,我们需要获得 距离上次刷新过了多少时间 後面简称 Delta Time

-- src/main.ts

app.ticker.add(() => {
  app.stage.removeChildren();

- app.stage.addChild(update());
+ app.stage.addChild(update(app.ticker.deltaMS));
});

透过 app.ticker.deltaMS 我们获得了 Delta Time
将它抛进 update 函式後,

-- src/characters/Crab.ts

export default function Crab() {
  let current = 0;
  const images = [image, image2];

+ let timePass = 0;

- return () => {
+ return (delta: number) => {
+   timePass += delta;

    const image = images[current % images.length];
-   current += 1;
+   if (timePass > 1000) {
+      current += 1;
+      timePass = 0;
+   }

    const graphics = new Graphics();

    for (let y = 0; y < image.length; y++) {
      for (let x = 0; x < image[y].length; x++) {
        if (image[y][x] === 0) continue;

        graphics.beginFill(0xffffff);

        graphics.drawRect(x, y, 1, 1);

        graphics.endFill();
      }
    }

    return graphics;
  };
}

这样就只会每 1 秒 切换一次图片了!

最後结果如下:

Final

小考题

以下提供其他角色的图片,大家可以自行实作看看!

Crab

const image1 = [
  [0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0],
  [0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0],
  [0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0],
  [0, 1, 1, 0, 1, 1, 1, 0, 1, 1, 0],
  [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
  [1, 0, 1, 1, 1, 1, 1, 1, 1, 0, 1],
  [1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 1],
  [0, 0, 0, 1, 1, 0, 1, 1, 0, 0, 0],
];

const image2 = [
  [0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0],
  [1, 0, 0, 1, 0, 0, 0, 1, 0, 0, 1],
  [1, 0, 1, 1, 1, 1, 1, 1, 1, 0, 1],
  [1, 1, 1, 0, 1, 1, 1, 0, 1, 1, 1],
  [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
  [0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0],
  [0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0],
  [0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0],
];

Octopus

const image1 = [
  [0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0],
  [0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0],
  [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
  [1, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 1],
  [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
  [0, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 0],
  [0, 0, 1, 1, 0, 1, 1, 0, 1, 1, 0, 0],
  [1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1],
];

const image2 = [
  [0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0],
  [0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0],
  [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
  [1, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 1],
  [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
  [0, 0, 1, 1, 1, 0, 0, 1, 1, 1, 0, 0],
  [0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0],
  [0, 0, 1, 1, 0, 0, 0, 0, 1, 1, 0, 0],
];

Squid

const image1 = [
  [0, 0, 0, 1, 1, 0, 0, 0],
  [0, 0, 1, 1, 1, 1, 0, 0],
  [0, 1, 1, 1, 1, 1, 1, 0],
  [1, 1, 0, 1, 1, 0, 1, 1],
  [1, 1, 1, 1, 1, 1, 1, 1],
  [0, 0, 1, 0, 0, 1, 0, 0],
  [0, 1, 0, 1, 1, 0, 1, 0],
  [1, 0, 1, 0, 0, 1, 0, 1],
];

const image2 = [
  [0, 0, 0, 1, 1, 0, 0, 0],
  [0, 0, 1, 1, 1, 1, 0, 0],
  [0, 1, 1, 1, 1, 1, 1, 0],
  [1, 1, 0, 1, 1, 0, 1, 1],
  [1, 1, 1, 1, 1, 1, 1, 1],
  [0, 1, 0, 1, 1, 0, 1, 0],
  [1, 0, 0, 0, 0, 0, 0, 1],
  [0, 1, 0, 0, 0, 0, 1, 0],
];

关於兔兔们:


<<:  Day 4 ( 入门 ) 一直向下的箭头

>>:  每个人都该学的30个Python技巧|技巧 4:字串格式化(字幕、衬乐、练习)

Android Studio初学笔记-Day21-AlertDialog(2)

接续上一篇AlertDialog的基础介绍,今天来接着介绍更多AlertDialog的用法。 从上一...

Day 28 - styled-components 笔记3

Q_Q .. 对预设建立的 component 延伸自订样式 import styled from...

App 在发布到play商店後 Firebase Authentication 无法登入问题解决

身为一个App的开发新手常常会遇到一些莫名其妙又难以解决的问题,直到找到问题答案才发现根本是自己愚蠢...

《征服》魔术师是万无一失

解读《征服》这本书,很有意思的是其中的时间管理,注意力管理,还有风险管理的维度展开。 把我们对魔术师...

[Day 24] Leetcode 416. Partition Equal Subset Sum (C++)

前言 今天继续挑战top 100 liked中sum相关的题目─416. Partition Equ...