回到首页 返回首页
回到顶部 回到顶部
返回上一页 返回上一页

【花雕动手做】Kitronik 可编程开发板基于 ARCADE MakeCode 之漫天烟花 简单

头像 驴友花雕 2025.09.10 3 0

Kitronik ARCADE 是一款由英国教育科技公司 Kitronik 精心打造的可编程游戏机开发板,专为编程教学与创客实践而设计。该设备原生支持微软的 MakeCode Arcade 平台,用户可通过图形化或 JavaScript 编程方式,轻松创建、下载并运行复古风格的街机游戏。

它集成了彩色 LCD 显示屏、方向控制键、功能按键、蜂鸣器和震动马达等交互组件,提供完整的游戏输入输出体验。无论是初学者进行编程启蒙,还是创客群体开发交互式作品,Kitronik ARCADE 都能作为理想的硬件载体,助力创意实现。

凭借其开源友好、易于上手、兼容性强等特点,该开发板广泛应用于中小学编程课程、创客工作坊、游戏开发教学以及个人项目原型设计,深受教育者与技术爱好者的喜爱。

 

00 (2).jpg
00 (3).jpg
00 (4).jpg

作为学习、练习与尝试,这里创建一个漫天烟花的小游戏。
打开网页版:https://arcade.makecode.com/,设置项目名称:漫天烟花

JavaScript 实验参考代码

 

代码
const fireworkEffects: effects.ParticleEffect[] = [
    /** small spinner effect **/
    createEffect(
        1000,
        300,
        () => {
            /**
             * this extends the radial factory used in the warm radial, cool radial,
             * and halo effects to shorten the lifespan of the particles, so they will
             * form a smaller radius
             */
            class ShortRadial extends particles.RadialFactory {
                createParticle(anchor: particles.ParticleAnchor) {
                    const p = super.createParticle(anchor);
                    p.lifespan = randint(200, 450);
                    return p;
                }
            }

            return new ShortRadial(
                2,
                50,
                5,
                randomPalette(randint(2, 5))
            );
        }
    ),
    /** Brocade: forms an 'umbrella like' pattern. I started building this off of the 'fountain' particle **/
    new effects.ParticleEffect(600, 500, function (anchor: particles.ParticleAnchor, particlesPerSecond: number) {
        class BrocadeFactory extends particles.SprayFactory {
            galois: Math.FastRandom;
            palette: number[];

            constructor() {
                super(110, 180, 359);
                this.galois = new Math.FastRandom();
                this.palette = randomPalette(2);
            }

            createParticle(anchor: particles.ParticleAnchor) {
                const p = super.createParticle(anchor);

                if (this.galois.percentChance(25)) {
                    p.color = this.palette[0];
                    p.lifespan = randint(50, 150);
                } else {
                    p.color = this.palette[1];
                    p.lifespan = randint(50, 350);
                }
                return p;
            }

            drawParticle(p: particles.Particle, x: Fx8, y: Fx8) {
                // always just fill a pixel if color is first color, otherwise single pixel 3/4 the time
                if (p.color == this.palette[0] || this.galois.percentChance(85)) {
                    screen.setPixel(Fx.toInt(x), Fx.toInt(y), p.color);
                } else {
                    const toPrint = this.galois.randomBool()
                        ? img`
                            . 1 .
                            1 1 1
                            . 1 .
                        `
                        : img`
                            1 . 1
                            . 1 .
                            1 . 1
                        `;
                    toPrint.replace(0x1, p.color);
                    screen.drawTransparentImage(
                        toPrint,
                        Fx.toInt(x),
                        Fx.toInt(y)
                    );
                }
            }
        }

        const factory = new BrocadeFactory();
        const source = new particles.ParticleSource(anchor, particlesPerSecond, factory);
        source.setAcceleration(0, 600);
        return source;
    }),
    /** Sparkler like effect**/
    createEffect(
        600,
        600,
        () => {
            class SparklerFactory extends particles.SprayFactory {
                galois: Math.FastRandom;
                palette: number[];

                constructor() {
                    super(50, 180, 359);
                    this.galois = new Math.FastRandom();
                    this.palette = randomPalette(2);
                }

                createParticle(anchor: particles.ParticleAnchor) {
                    const p = super.createParticle(anchor);
                    p.data = randint(0, 10);

                    if (this.galois.percentChance(25)) {
                        p.color = this.palette[0];
                        p.lifespan = randint(250, 450);
                    } else {
                        p.color = this.palette[2];
                        p.lifespan = randint(500, 750);
                    }

                    return p;
                }

                drawParticle(p: particles.Particle, x: Fx8, y: Fx8) {
                    ++p.data;
                    // this condition will make the particles flicker;
                    // p.data >> 1 is equivalent to dividing by 2,
                    // and % 2 evaluates to 1 or 0 (effectively, odd or even)
                    // this condition then executes if it evaluates to 1,
                    // which javascript considers to be 'truthy'
                    if ((p.data >> 1) % 2) {
                        // mostly print single dots, but potentially also print small shapes
                        const toPrint = this.galois.percentChance(90)
                            ? img`1`
                            : this.galois.randomBool()
                                ? img`
                                    . 1 .
                                    1 . 1
                                    . 1 .
                                `
                                : img`
                                    1 . 1
                                    . 1 .
                                `;
                        toPrint.replace(1, p.color);
                        screen.drawTransparentImage(
                            toPrint,
                            Fx.toInt(x),
                            Fx.toInt(y)
                        );
                    }
                }
            }

            return new SparklerFactory();
        }
    ),
    /** Crossette: straight lines that fly straight out, with small 'branches' **/
    createEffect(
        100,
        600,
        () => {
            class CrossetteFactory extends particles.SprayFactory {
                galois: Math.FastRandom;
                anchor: particles.ParticleAnchor;
                particlesRemaining: number
                palette: number[];

                constructor() {
                    super(40, 180, 359);
                    this.galois = new Math.FastRandom();
                    this.particlesRemaining = 8;
                    this.palette = randomPalette(2);
                }

                createParticle(anchor: particles.ParticleAnchor) {
                    if (--this.particlesRemaining < 0) {
                        return undefined;
                    }
                    if (!this.anchor)
                        this.anchor = anchor;
                    const p = super.createParticle(anchor);
                    const particleRateMultiple = Fx8(randint(60, 100) / 100);
                    p.vx = Fx.mul(p.vx, particleRateMultiple);
                    p.vy = Fx.mul(p.vy, particleRateMultiple);
                    p.color = this.palette[this.galois.randomRange(0, 1)];;

                    p.lifespan = randint(600, 800);
                    return p;
                }

                drawParticle(p: particles.Particle, x: Fx8, y: Fx8) {
                    // double line with offset x to make the current position of the particle
                    // slightly 'thicker'
                    for (let i = 0; i < 2; i++) {
                        screen.drawLine(
                            Fx.toInt(x) + i,
                            Fx.toInt(y),
                            this.anchor.x,
                            this.anchor.y,
                            p.color
                        );
                    }
                    if (this.galois.randomBool()) {
                        screen.drawTransparentImage(
                            this.galois.randomBool()
                                ? img`
                                    4 . 4
                                    . 4 .
                                    4 . 4
                                `
                                : img`
                                    . 4 .
                                    4 . 4
                                    . 4 .
                                `,
                            Fx.toInt(x) - 1,
                            Fx.toInt(y) - 1
                        );
                    }
                }
            }

            return new CrossetteFactory();
        }
    ),
]

/**
 * This is copied from my original definition for it in
 * pxt-common-packages/libs/game/particleeffects.ts, as that isn't currently exported.
 *
 * It is used to wrap simple particle factories that are created with a standard source
 * into effects that can be easily used
 */
function createEffect(
    defaultParticlesPerSecond: number,
    defaultLifespan: number,
    factoryFactory: (anchor?: particles.ParticleAnchor) => particles.ParticleFactory
): effects.ParticleEffect {
    return new effects.ParticleEffect(defaultParticlesPerSecond, defaultLifespan,
        (anchor: particles.ParticleAnchor, pps: number) =>
            new particles.ParticleSource(anchor, pps, factoryFactory()));
}

// stars that don't twinkle - focus should be on fireworks, not the random
// changes in the background
new effects.ScreenEffect(
    2,
    5,
    5000,
    function (anchor: particles.ParticleAnchor, particlesPerSecond: number) {
        class NoTwinkleStarFactory extends particles.StarFactory {
            constructor() {
                super();
                this.possibleColors = [0xE, 0xB, 0xC, 0xD];
            }

            drawParticle(p: particles.Particle, x: Fx8, y: Fx8) {
                const rest = 0x7FFF;
                const selected = this.images[rest & p.data].clone();
                selected.replace(0x1, p.color);
                screen.drawTransparentImage(
                    selected,
                    Fx.toInt(x),
                    Fx.toInt(y)
                );
            }
        }
        const factory = new NoTwinkleStarFactory();
        return new particles.ParticleSource(
            anchor,
            particlesPerSecond,
            new NoTwinkleStarFactory()
        );
    }
).startScreenEffect();

const fireworkTrail = createEffect(
    25,
    50,
    a => {
        class FireworkTrail extends particles.ParticleFactory {
            constructor() {
                super();
            }

            createParticle(anchor: particles.ParticleAnchor) {
                const p = super.createParticle(anchor);
                p.vx = Fx.neg(Fx8(anchor.vx + randint(-10, 10)));
                p.vy = Fx.neg(Fx8(anchor.vy >> 1));
                p.lifespan = randint(50, 500);
                p.color = Math.percentChance(90) ? 0xE : randint(0x1, 0xD);
                return p;
            }

            drawParticle(p: particles.Particle, x: Fx8, y: Fx8) {
                screen.setPixel(
                    Fx.toInt(x),
                    Fx.toInt(y),
                    p.color
                );
            }
        }
        return new FireworkTrail;
    }
);

// disable the menu button - menus shouldn't get in the way of the demonstration!
controller.menu.onEvent(ControllerButtonEvent.Pressed, undefined);
controller.anyButton.onEvent(
    ControllerButtonEvent.Pressed,
    tryToFire
);

const TIMEOUT = 200;
let lastFired = game.runtime();
function tryToFire() {
    const time = game.runtime();
    if (lastFired + TIMEOUT < time) {
        const vx = randint(-35, 35);
        const firework = sprites.createProjectileFromSide(
            img`
                e
                e
            `,
            vx,
            randint(-150, -125)
        );

        if (!firework.vx || Math.percentChance(70)) {
            firework.x = randint(25, screen.width - 25);
        } else {
            firework.y -= 20;
            firework.vy *= .8;
            if (Math.abs(firework.vx) < 10) {
                firework.vx = randint(30, 40) * (firework.vx < 0 ? -1 : 1);
            } else {
                firework.vx *= 2;
            }
        }

        firework.startEffect(fireworkTrail);
        firework.ay = 100;
        firework.lifespan = randint(800, 1200);
        lastFired = time;
    }
}

game.onUpdate(function () {
    if (lastFired + (3 * TIMEOUT) < game.runtime()) {
        // auto fire if there hasn't been any for a while
        tryToFire();
    }
});


sprites.onDestroyed(SpriteKind.Projectile, s => {
    Math.pickRandom(fireworkEffects).start(s, 500);
});

/**
 * Color stuff
 *
 * this uses the pxt-color extension to change the color palette at runtime.
 * To make all the fireworks unique, this generates a random palette of pastel-ish colors,
 * with the exception of 0xE (set to white) and 0xF (left as black).
 * It also continuously changes the colors from 0x1 to 0xA, fading between different palettes
 * for those colors at random.
 */
const p = color.currentPalette();
p.setColor(0xB, generatePastel().hexValue());
p.setColor(0xC, generatePastel().hexValue());
p.setColor(0xD, generatePastel().hexValue());
p.setColor(0xE, 0xFFFFFF);
color.setPalette(p);

forever(() => {
    new color.Fade()
        .mapEndHSL(
            generatePastel,
            0x1,
            0xA
        )
        .startUntilDone(500);
})

function generatePastel() {
    // generate a random pastel-adjacent color:
    // pastels have 100% saturation and high luminosity ('brightness')
    return new color.HSL(
        randint(0, 359),
        1,
        randint(75, 95) / 100
    );
}

/**
 * Generates a value to be used to specify the colors for each firework,
 * so that the colors aren't always the same between fireworks that run
 * at the same time (value between 1 and 8, so there will always be )
 */
function randomPalette(len: number) {
    if (len > 8) {
        len = 8;
    }
    const palette: number[] = [];
    for (let i = 0; i < len; i++) {
        while (palette.length == i) {
            const selected = randint(1, 0xA);
            if (palette.indexOf(selected) < 0) {
                palette.push(selected);
            }
        }
    }

    return palette;
}

ARCADE MakeCode 漫天烟花模拟代码解读

这是一个精美的烟花模拟程序,使用MakeCode Arcade的粒子系统创建了多种烟花效果。

 

程序概述

这是一个交互式烟花模拟器,玩家可以按下任何按钮发射烟花,烟花会在空中爆炸并产生多种华丽的粒子效果。程序包含多种烟花类型、颜色渐变和自动发射机制。

核心代码解析

1. 烟花效果定义

程序定义了4种不同的烟花效果:

1.1 小型旋转效果 (Small Spinner)

typescript

createEffect(1000, 300, () => {    class ShortRadial extends particles.RadialFactory {        createParticle(anchor: particles.ParticleAnchor) {            const p = super.createParticle(anchor);            p.lifespan = randint(200, 450); // 缩短粒子寿命形成小半径            return p;        }    }    return new ShortRadial(2, 50, 5, randomPalette(randint(2, 5))); })

1.2 锦缎效果 (Brocade)

typescript

new effects.ParticleEffect(600, 500, function (anchor, particlesPerSecond) {    class BrocadeFactory extends particles.SprayFactory {        // 创建"伞状"图案,25%概率使用主色,75%概率使用副色        createParticle(anchor) {            if (this.galois.percentChance(25)) {                p.color = this.palette[0];                p.lifespan = randint(50, 150);            } else {                p.color = this.palette[1];                p.lifespan = randint(50, 350);            }        }    } })

1.3 火花效果 (Sparkler)

typescript

createEffect(600, 600, () => {    class SparklerFactory extends particles.SprayFactory {        // 创建闪烁的火花效果,粒子会闪烁显示        drawParticle(p, x, y) {            if ((p.data >> 1) % 2) { // 通过位移操作实现闪烁                // 90%概率显示单像素,10%概率显示小形状            }        }    } })

1.4 十字效果 (Crossette)

typescript

createEffect(100, 600, () => {    class CrossetteFactory extends particles.SprayFactory {        // 创建直线飞行带小分支的效果        drawParticle(p, x, y) {            // 绘制双线使粒子位置更粗            screen.drawLine(x, y, this.anchor.x, this.anchor.y, p.color);        }    } })

2. 辅助函数

createEffect 包装器

typescript

function createEffect(defaultParticlesPerSecond, defaultLifespan, factoryFactory) {    // 将简单粒子工厂包装成易于使用的效果    return new effects.ParticleEffect(defaultParticlesPerSecond, defaultLifespan,        (anchor, pps) => new particles.ParticleSource(anchor, pps, factoryFactory())); }

3. 背景效果

不闪烁的星星

typescript

new effects.ScreenEffect(2, 5, 5000, function (anchor, particlesPerSecond) {    class NoTwinkleStarFactory extends particles.StarFactory {        // 覆盖draw方法,创建不闪烁的星星        drawParticle(p, x, y) {            const selected = this.images[rest & p.data].clone();            selected.replace(0x1, p.color);            screen.drawTransparentImage(selected, x, y);        }    } }).startScreenEffect();

4. 烟花轨迹效果

typescript

const fireworkTrail = createEffect(25, 50, a => {    class FireworkTrail extends particles.ParticleFactory {        createParticle(anchor) {            p.vx = Fx.neg(Fx8(anchor.vx + randint(-10, 10))); // 反向速度加随机偏移            p.vy = Fx.neg(Fx8(anchor.vy >> 1)); // 垂直速度减半并反向            p.color = Math.percentChance(90) ? 0xE : randint(0x1, 0xD); // 90%白色,10%随机色        }    } });

5. 发射控制

按钮控制

typescript

controller.anyButton.onEvent(ControllerButtonEvent.Pressed, tryToFire); function tryToFire() {    if (lastFired + TIMEOUT < time) {        const firework = sprites.createProjectileFromSide(img`e e`, vx, randint(-150, -125));        firework.startEffect(fireworkTrail);        firework.ay = 100; // 重力加速度        firework.lifespan = randint(800, 1200); // 生存时间    } }

自动发射

typescript

game.onUpdate(function () {    if (lastFired + (3 * TIMEOUT) < game.runtime()) {        tryToFire(); // 如果一段时间没有发射,自动发射    } });

6. 爆炸效果

typescript

sprites.onDestroyed(SpriteKind.Projectile, s => {    Math.pickRandom(fireworkEffects).start(s, 500); // 烟花销毁时随机选择一种爆炸效果 });

7. 颜色系统

调色板设置

typescript

const p = color.currentPalette(); p.setColor(0xB, generatePastel().hexValue()); p.setColor(0xC, generatePastel().hexValue()); p.setColor(0xD, generatePastel().hexValue()); p.setColor(0xE, 0xFFFFFF); // 白色 color.setPalette(p);

颜色渐变

typescript

forever(() => {    new color.Fade()        .mapEndHSL(generatePastel, 0x1, 0xA) // 在颜色1-10之间渐变        .startUntilDone(500); }) function generatePastel() {    return new color.HSL(        randint(0, 359), // 随机色相        1,               // 100%饱和度        randint(75, 95) / 100 // 75-95%亮度(pastel色调)    ); }

随机调色板生成

typescript

function randomPalette(len: number) {    const palette: number[] = [];    for (let i = 0; i < len; i++) {        const selected = randint(1, 0xA); // 从颜色1-10中随机选择        if (palette.indexOf(selected) < 0) { // 确保不重复            palette.push(selected);        }    }    return palette; }

技术特点

高级粒子系统:使用MakeCode的粒子系统创建复杂效果

面向对象设计:通过类继承创建自定义粒子工厂

颜色管理:动态调色板和HSL颜色空间使用

物理模拟:模拟重力、速度、加速度等物理效果

随机化:大量使用随机数创建多样化效果

性能优化:通过粒子生命周期管理优化性能

视觉效果

多种烟花类型:4种不同的爆炸效果

颜色渐变:背景颜色持续渐变

粒子多样性:不同形状、颜色、寿命的粒子

轨迹效果:烟花上升时的尾迹

星空背景:不闪烁的星星背景

通过模拟器,调试与模拟运行

 

00206-.gif

 

实验场景记录

 

88 (1).jpg
88 (2).jpg

评论

user-avatar
icon 他的勋章
    展开更多