在 Space Invaders
的游戏设计中,
除了随着不断前进而产生的压迫感之外,
Enemy
的射击也扮演了相辅相成的作用。
他让游戏内容不只是单纯的玩家射击,
玩家也需要小心来自敌方的攻击,
而且随着时间的推移,敌人与玩家的距离缩短,难度跟刺激感会越来越强烈,
这是这款游戏最精妙的设计。
接下来,卡比要实作原作游戏中,Enemy
的射击逻辑。
首先我们需要新增一个新的档案,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
。
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;
};
}
接着就是,主要的高阶函式,
第一个参数 delta
,也就是每祯画面的时间差,
第二个参数就是画面上的 instances
。
透过 Enemy
的 id
我们很快就可以过滤出哪一排,
并挑出最接近玩家的 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;
};
}
接下来我们要写一个方便的工具,
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 发信!
完成後来试着用这些组建建立一个基本的 Dashbord 看起来还有很多要调整的地方... 在使用上 ...
天啊~终於到了铁人赛的最後一天,威尔猪成功插旗了。第一次参加铁人赛就历经艰辛,甚至可以说是惊心动魄...
今天大概会聊到的范围 Snapshot system 上一篇有提到,State 改变时会触发 re...
大家好! 今天要实作能输入网址的 RSS 阅读器。 我们进入今天的主题吧! 程序码 Felix('f...
令人崩溃的期末专题进行了两个礼拜,终於在茫然的浑沌中摸索出一些头绪,对规划工作和时辰安排有比较好的掌...