[Day15] Vite 出小蜜蜂~随机射击 Randomly Shoot!

Day15

Space Invaders 的游戏设计中,
除了随着不断前进而产生的压迫感之外,
Enemy 的射击也扮演了相辅相成的作用。

他让游戏内容不只是单纯的玩家射击,
玩家也需要小心来自敌方的攻击,
而且随着时间的推移,敌人与玩家的距离缩短,难度跟刺激感会越来越强烈,
这是这款游戏最精妙的设计。

接下来,卡比要实作原作游戏中,Enemy 的射击逻辑。

Random

首先我们需要新增一个新的档案,src/logic/RandomlyShoot.ts

然後,我们撰写一个随机函式,帮助我们挑选由 哪一排 进行射击。

function getRandomInt(min: number, max: number) {
  min = Math.ceil(min);
  max = Math.floor(max);
  return Math.floor(Math.random() * (max - min) + min);
}

Throttle

根据原作游戏,大概每隔一秒会进行射击。

我们透过每次刷新画面时的时间差,计算经过的秒数,
当秒数超过指定时间後,才去执行函式,
这个概念叫做 throttle

function throttle(ms: number, fn: Function) {
  let duration = ms;

  return function (delta: number, ...args: any[]) {
    duration -= delta;

    if (duration > 0) return;

    fn(delta, ...args);
    duration = ms;
  };
}

Randomly Shoot

接着就是,主要的高阶函式,
第一个参数 delta,也就是每祯画面的时间差,
第二个参数就是画面上的 instances

透过 Enemyid 我们很快就可以过滤出哪一排,
并挑出最接近玩家的 Enemy 即可。

-- src/logic/RandomlyShoot.ts

import { isEnemy } from "../characters/Enemy";
import { GameObject } from "../types";

function getRandomInt(min: number, max: number) {
  min = Math.ceil(min);
  max = Math.floor(max);
  return Math.floor(Math.random() * (max - min) + min);
}

function throttle(ms: number, fn: Function) {
  let duration = ms;

  return function (delta: number, ...args: any[]) {
    duration -= delta;

    if (duration > 0) return;

    fn(delta, ...args);
    duration = ms;
  };
}

type Props = {
  row: number;
  rate: number;
};
export function RandomlyShoot({ row, rate }: Props) {
  return throttle(rate, (_: number, instances: GameObject[]) => {
    const x = getRandomInt(0, row);

    const instance = instances
      .filter(isEnemy)
      .filter(({ id }) => (id % row) - x === 0)
      .sort((a, b) => b.position.y - a.position.y)[0];

    if (instance) instance.canShoot = true;
  });
}

因为我们调整了参数,所以我们要修正一下 SequentialMovement
这边会在後面章节再进行调整,方便我们每次改动时更有弹性。

export function SequentialMovement({ counts, step }: Props) {
  const movement = { x: step, y: 0 };
  let pedometer = 0;
  let index = 0;

+ return (_: number, instances: GameObject[]) => {
    const enemies = instances.filter(isEnemy);

    let processed = enemies.length > 0;

    while (processed) {
      enemies
        .filter((instance) => instance.id === index)
        .forEach((instance) => {
          instance.position.x += movement.x;
          instance.position.y += movement.y;
          instance.frame += 1;

          processed = false;
        });

      index = (index + 1) % counts;
    }

    if (index === 0) {
      if (pedometer === 0) movement.y = 0;

      pedometer += 1;
    }

    if (pedometer <= 10) return;

    movement.x *= -1;
    movement.y = step;

    pedometer = 0;
  };
}

Functional Programming - ap

接下来我们要写一个方便的工具,
ap,他可以把传入的函式组合成一个,
我们只需要执行那个组合後的函式就可以了。

const ap = (...fns: Function[]) => (...args: any[]) =>
  fns.reduce((res, fn) => res.concat(fn(...args)), [] as any[]);
+ import { RandomlyShoot } from "../logic/RandomlyShoot";

+ const ap = (...fns: Function[]) => (...args: any[]) =>
+   fns.reduce((res, fn) => res.concat(fn(...args)), [] as any[]);

export default function Game(screen: Rectangle): Scene<Container> {
  let instances: GameObject[] = [LaserCannon(screen), ...spawn(Enemy, points)];

+ const update = ap(
+   SequentialMovement({
+     counts: instances.filter(isEnemy).length,
+     step: 2,
+   }),
+   RandomlyShoot({
+     row: ROW_WIDTH,
+     rate: 1000,
+   })
+ );

  return {
    update(delta) {
      collisionDetect(instances.filter(canCollision).filter(canTransform));

+     update(delta, instances);

      instances.forEach((instance) => {
        if (canControl(instance)) {
          instance.handleInput(getKeyPressed());
        }

        if (canShoot(instance) && instance.canShoot) {
          requestAnimationFrame(() => {
            instances = [...instances, instance.shoot()];
          });

          instance.canShoot = false;
        }

        if (instance.destroy) {
          requestAnimationFrame(() => {
            instances = instances.filter((_instance) => _instance !== instance);
          });

          return;
        }

        instance.update?.(delta);
      });
    },

    render(stage) {
      clear();

      instances
        .filter(canRender)
        .forEach((instance) => render(stage, instance));
    },
  };
}

关於兔兔们:


<<:  【Day 13】颜立委:希望你们都学会,如何透过 SMTP 发信!

>>:  很像JavaScript 的 JSON

Day 30 建立 Dashboard 与部属 storybook

完成後来试着用这些组建建立一个基本的 Dashbord 看起来还有很多要调整的地方... 在使用上 ...

Day 30 - [最终章] 完赛心路历程

天啊~终於到了铁人赛的最後一天,威尔猪成功插旗了。第一次参加铁人赛就历经艰辛,甚至可以说是惊心动魄...

D25 / 为什麽 State 改变会触发 recomposition - State & Snapshot system

今天大概会聊到的范围 Snapshot system 上一篇有提到,State 改变时会触发 re...

JS 44 - 输入网址就能使用的 RSS 阅读器

大家好! 今天要实作能输入网址的 RSS 阅读器。 我们进入今天的主题吧! 程序码 Felix('f...

{CMoney战斗营} 的期末专题 # 前後端分离

令人崩溃的期末专题进行了两个礼拜,终於在茫然的浑沌中摸索出一些头绪,对规划工作和时辰安排有比较好的掌...