13浏览
查看: 13|回复: 3

[项目] 【花雕动手做】基于 Kitronik 可编程开发板之漫天烟花

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

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

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

【花雕动手做】基于 Kitronik 可编程开发板之漫天烟花图2

【花雕动手做】基于 Kitronik 可编程开发板之漫天烟花图3

【花雕动手做】基于 Kitronik 可编程开发板之漫天烟花图1

【花雕动手做】基于 Kitronik 可编程开发板之漫天烟花图4

驴友花雕  中级技神
 楼主|

发表于 4 小时前

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

本帖最后由 驴友花雕 于 2025-9-10 18:53 编辑

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

JavaScript 实验参考代码

  1. const fireworkEffects: effects.ParticleEffect[] = [
  2.     /** small spinner effect **/
  3.     createEffect(
  4.         1000,
  5.         300,
  6.         () => {
  7.             /**
  8.              * this extends the radial factory used in the warm radial, cool radial,
  9.              * and halo effects to shorten the lifespan of the particles, so they will
  10.              * form a smaller radius
  11.              */
  12.             class ShortRadial extends particles.RadialFactory {
  13.                 createParticle(anchor: particles.ParticleAnchor) {
  14.                     const p = super.createParticle(anchor);
  15.                     p.lifespan = randint(200, 450);
  16.                     return p;
  17.                 }
  18.             }
  19.             return new ShortRadial(
  20.                 2,
  21.                 50,
  22.                 5,
  23.                 randomPalette(randint(2, 5))
  24.             );
  25.         }
  26.     ),
  27.     /** Brocade: forms an 'umbrella like' pattern. I started building this off of the 'fountain' particle **/
  28.     new effects.ParticleEffect(600, 500, function (anchor: particles.ParticleAnchor, particlesPerSecond: number) {
  29.         class BrocadeFactory extends particles.SprayFactory {
  30.             galois: Math.FastRandom;
  31.             palette: number[];
  32.             constructor() {
  33.                 super(110, 180, 359);
  34.                 this.galois = new Math.FastRandom();
  35.                 this.palette = randomPalette(2);
  36.             }
  37.             createParticle(anchor: particles.ParticleAnchor) {
  38.                 const p = super.createParticle(anchor);
  39.                 if (this.galois.percentChance(25)) {
  40.                     p.color = this.palette[0];
  41.                     p.lifespan = randint(50, 150);
  42.                 } else {
  43.                     p.color = this.palette[1];
  44.                     p.lifespan = randint(50, 350);
  45.                 }
  46.                 return p;
  47.             }
  48.             drawParticle(p: particles.Particle, x: Fx8, y: Fx8) {
  49.                 // always just fill a pixel if color is first color, otherwise single pixel 3/4 the time
  50.                 if (p.color == this.palette[0] || this.galois.percentChance(85)) {
  51.                     screen.setPixel(Fx.toInt(x), Fx.toInt(y), p.color);
  52.                 } else {
  53.                     const toPrint = this.galois.randomBool()
  54.                         ? img`
  55.                             . 1 .
  56.                             1 1 1
  57.                             . 1 .
  58.                         `
  59.                         : img`
  60.                             1 . 1
  61.                             . 1 .
  62.                             1 . 1
  63.                         `;
  64.                     toPrint.replace(0x1, p.color);
  65.                     screen.drawTransparentImage(
  66.                         toPrint,
  67.                         Fx.toInt(x),
  68.                         Fx.toInt(y)
  69.                     );
  70.                 }
  71.             }
  72.         }
  73.         const factory = new BrocadeFactory();
  74.         const source = new particles.ParticleSource(anchor, particlesPerSecond, factory);
  75.         source.setAcceleration(0, 600);
  76.         return source;
  77.     }),
  78.     /** Sparkler like effect**/
  79.     createEffect(
  80.         600,
  81.         600,
  82.         () => {
  83.             class SparklerFactory extends particles.SprayFactory {
  84.                 galois: Math.FastRandom;
  85.                 palette: number[];
  86.                 constructor() {
  87.                     super(50, 180, 359);
  88.                     this.galois = new Math.FastRandom();
  89.                     this.palette = randomPalette(2);
  90.                 }
  91.                 createParticle(anchor: particles.ParticleAnchor) {
  92.                     const p = super.createParticle(anchor);
  93.                     p.data = randint(0, 10);
  94.                     if (this.galois.percentChance(25)) {
  95.                         p.color = this.palette[0];
  96.                         p.lifespan = randint(250, 450);
  97.                     } else {
  98.                         p.color = this.palette[2];
  99.                         p.lifespan = randint(500, 750);
  100.                     }
  101.                     return p;
  102.                 }
  103.                 drawParticle(p: particles.Particle, x: Fx8, y: Fx8) {
  104.                     ++p.data;
  105.                     // this condition will make the particles flicker;
  106.                     // p.data >> 1 is equivalent to dividing by 2,
  107.                     // and % 2 evaluates to 1 or 0 (effectively, odd or even)
  108.                     // this condition then executes if it evaluates to 1,
  109.                     // which javascript considers to be 'truthy'
  110.                     if ((p.data >> 1) % 2) {
  111.                         // mostly print single dots, but potentially also print small shapes
  112.                         const toPrint = this.galois.percentChance(90)
  113.                             ? img`1`
  114.                             : this.galois.randomBool()
  115.                                 ? img`
  116.                                     . 1 .
  117.                                     1 . 1
  118.                                     . 1 .
  119.                                 `
  120.                                 : img`
  121.                                     1 . 1
  122.                                     . 1 .
  123.                                 `;
  124.                         toPrint.replace(1, p.color);
  125.                         screen.drawTransparentImage(
  126.                             toPrint,
  127.                             Fx.toInt(x),
  128.                             Fx.toInt(y)
  129.                         );
  130.                     }
  131.                 }
  132.             }
  133.             return new SparklerFactory();
  134.         }
  135.     ),
  136.     /** Crossette: straight lines that fly straight out, with small 'branches' **/
  137.     createEffect(
  138.         100,
  139.         600,
  140.         () => {
  141.             class CrossetteFactory extends particles.SprayFactory {
  142.                 galois: Math.FastRandom;
  143.                 anchor: particles.ParticleAnchor;
  144.                 particlesRemaining: number
  145.                 palette: number[];
  146.                 constructor() {
  147.                     super(40, 180, 359);
  148.                     this.galois = new Math.FastRandom();
  149.                     this.particlesRemaining = 8;
  150.                     this.palette = randomPalette(2);
  151.                 }
  152.                 createParticle(anchor: particles.ParticleAnchor) {
  153.                     if (--this.particlesRemaining < 0) {
  154.                         return undefined;
  155.                     }
  156.                     if (!this.anchor)
  157.                         this.anchor = anchor;
  158.                     const p = super.createParticle(anchor);
  159.                     const particleRateMultiple = Fx8(randint(60, 100) / 100);
  160.                     p.vx = Fx.mul(p.vx, particleRateMultiple);
  161.                     p.vy = Fx.mul(p.vy, particleRateMultiple);
  162.                     p.color = this.palette[this.galois.randomRange(0, 1)];;
  163.                     p.lifespan = randint(600, 800);
  164.                     return p;
  165.                 }
  166.                 drawParticle(p: particles.Particle, x: Fx8, y: Fx8) {
  167.                     // double line with offset x to make the current position of the particle
  168.                     // slightly 'thicker'
  169.                     for (let i = 0; i < 2; i++) {
  170.                         screen.drawLine(
  171.                             Fx.toInt(x) + i,
  172.                             Fx.toInt(y),
  173.                             this.anchor.x,
  174.                             this.anchor.y,
  175.                             p.color
  176.                         );
  177.                     }
  178.                     if (this.galois.randomBool()) {
  179.                         screen.drawTransparentImage(
  180.                             this.galois.randomBool()
  181.                                 ? img`
  182.                                     4 . 4
  183.                                     . 4 .
  184.                                     4 . 4
  185.                                 `
  186.                                 : img`
  187.                                     . 4 .
  188.                                     4 . 4
  189.                                     . 4 .
  190.                                 `,
  191.                             Fx.toInt(x) - 1,
  192.                             Fx.toInt(y) - 1
  193.                         );
  194.                     }
  195.                 }
  196.             }
  197.             return new CrossetteFactory();
  198.         }
  199.     ),
  200. ]
  201. /**
  202. * This is copied from my original definition for it in
  203. * pxt-common-packages/libs/game/particleeffects.ts, as that isn't currently exported.
  204. *
  205. * It is used to wrap simple particle factories that are created with a standard source
  206. * into effects that can be easily used
  207. */
  208. function createEffect(
  209.     defaultParticlesPerSecond: number,
  210.     defaultLifespan: number,
  211.     factoryFactory: (anchor?: particles.ParticleAnchor) => particles.ParticleFactory
  212. ): effects.ParticleEffect {
  213.     return new effects.ParticleEffect(defaultParticlesPerSecond, defaultLifespan,
  214.         (anchor: particles.ParticleAnchor, pps: number) =>
  215.             new particles.ParticleSource(anchor, pps, factoryFactory()));
  216. }
  217. // stars that don't twinkle - focus should be on fireworks, not the random
  218. // changes in the background
  219. new effects.ScreenEffect(
  220.     2,
  221.     5,
  222.     5000,
  223.     function (anchor: particles.ParticleAnchor, particlesPerSecond: number) {
  224.         class NoTwinkleStarFactory extends particles.StarFactory {
  225.             constructor() {
  226.                 super();
  227.                 this.possibleColors = [0xE, 0xB, 0xC, 0xD];
  228.             }
  229.             drawParticle(p: particles.Particle, x: Fx8, y: Fx8) {
  230.                 const rest = 0x7FFF;
  231.                 const selected = this.images[rest & p.data].clone();
  232.                 selected.replace(0x1, p.color);
  233.                 screen.drawTransparentImage(
  234.                     selected,
  235.                     Fx.toInt(x),
  236.                     Fx.toInt(y)
  237.                 );
  238.             }
  239.         }
  240.         const factory = new NoTwinkleStarFactory();
  241.         return new particles.ParticleSource(
  242.             anchor,
  243.             particlesPerSecond,
  244.             new NoTwinkleStarFactory()
  245.         );
  246.     }
  247. ).startScreenEffect();
  248. const fireworkTrail = createEffect(
  249.     25,
  250.     50,
  251.     a => {
  252.         class FireworkTrail extends particles.ParticleFactory {
  253.             constructor() {
  254.                 super();
  255.             }
  256.             createParticle(anchor: particles.ParticleAnchor) {
  257.                 const p = super.createParticle(anchor);
  258.                 p.vx = Fx.neg(Fx8(anchor.vx + randint(-10, 10)));
  259.                 p.vy = Fx.neg(Fx8(anchor.vy >> 1));
  260.                 p.lifespan = randint(50, 500);
  261.                 p.color = Math.percentChance(90) ? 0xE : randint(0x1, 0xD);
  262.                 return p;
  263.             }
  264.             drawParticle(p: particles.Particle, x: Fx8, y: Fx8) {
  265.                 screen.setPixel(
  266.                     Fx.toInt(x),
  267.                     Fx.toInt(y),
  268.                     p.color
  269.                 );
  270.             }
  271.         }
  272.         return new FireworkTrail;
  273.     }
  274. );
  275. // disable the menu button - menus shouldn't get in the way of the demonstration!
  276. controller.menu.onEvent(ControllerButtonEvent.Pressed, undefined);
  277. controller.anyButton.onEvent(
  278.     ControllerButtonEvent.Pressed,
  279.     tryToFire
  280. );
  281. const TIMEOUT = 200;
  282. let lastFired = game.runtime();
  283. function tryToFire() {
  284.     const time = game.runtime();
  285.     if (lastFired + TIMEOUT < time) {
  286.         const vx = randint(-35, 35);
  287.         const firework = sprites.createProjectileFromSide(
  288.             img`
  289.                 e
  290.                 e
  291.             `,
  292.             vx,
  293.             randint(-150, -125)
  294.         );
  295.         if (!firework.vx || Math.percentChance(70)) {
  296.             firework.x = randint(25, screen.width - 25);
  297.         } else {
  298.             firework.y -= 20;
  299.             firework.vy *= .8;
  300.             if (Math.abs(firework.vx) < 10) {
  301.                 firework.vx = randint(30, 40) * (firework.vx < 0 ? -1 : 1);
  302.             } else {
  303.                 firework.vx *= 2;
  304.             }
  305.         }
  306.         firework.startEffect(fireworkTrail);
  307.         firework.ay = 100;
  308.         firework.lifespan = randint(800, 1200);
  309.         lastFired = time;
  310.     }
  311. }
  312. game.onUpdate(function () {
  313.     if (lastFired + (3 * TIMEOUT) < game.runtime()) {
  314.         // auto fire if there hasn't been any for a while
  315.         tryToFire();
  316.     }
  317. });
  318. sprites.onDestroyed(SpriteKind.Projectile, s => {
  319.     Math.pickRandom(fireworkEffects).start(s, 500);
  320. });
  321. /**
  322. * Color stuff
  323. *
  324. * this uses the pxt-color extension to change the color palette at runtime.
  325. * To make all the fireworks unique, this generates a random palette of pastel-ish colors,
  326. * with the exception of 0xE (set to white) and 0xF (left as black).
  327. * It also continuously changes the colors from 0x1 to 0xA, fading between different palettes
  328. * for those colors at random.
  329. */
  330. const p = color.currentPalette();
  331. p.setColor(0xB, generatePastel().hexValue());
  332. p.setColor(0xC, generatePastel().hexValue());
  333. p.setColor(0xD, generatePastel().hexValue());
  334. p.setColor(0xE, 0xFFFFFF);
  335. color.setPalette(p);
  336. forever(() => {
  337.     new color.Fade()
  338.         .mapEndHSL(
  339.             generatePastel,
  340.             0x1,
  341.             0xA
  342.         )
  343.         .startUntilDone(500);
  344. })
  345. function generatePastel() {
  346.     // generate a random pastel-adjacent color:
  347.     // pastels have 100% saturation and high luminosity ('brightness')
  348.     return new color.HSL(
  349.         randint(0, 359),
  350.         1,
  351.         randint(75, 95) / 100
  352.     );
  353. }
  354. /**
  355. * Generates a value to be used to specify the colors for each firework,
  356. * so that the colors aren't always the same between fireworks that run
  357. * at the same time (value between 1 and 8, so there will always be )
  358. */
  359. function randomPalette(len: number) {
  360.     if (len > 8) {
  361.         len = 8;
  362.     }
  363.     const palette: number[] = [];
  364.     for (let i = 0; i < len; i++) {
  365.         while (palette.length == i) {
  366.             const selected = randint(1, 0xA);
  367.             if (palette.indexOf(selected) < 0) {
  368.                 palette.push(selected);
  369.             }
  370.         }
  371.     }
  372.     return palette;
  373. }
复制代码


回复

使用道具 举报

驴友花雕  中级技神
 楼主|

发表于 4 小时前

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

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

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

核心代码解析
1. 烟花效果定义
程序定义了4种不同的烟花效果:

1.1 小型旋转效果 (Small Spinner)
typescript
  1. createEffect(1000, 300, () => {
  2.     class ShortRadial extends particles.RadialFactory {
  3.         createParticle(anchor: particles.ParticleAnchor) {
  4.             const p = super.createParticle(anchor);
  5.             p.lifespan = randint(200, 450); // 缩短粒子寿命形成小半径
  6.             return p;
  7.         }
  8.     }
  9.     return new ShortRadial(2, 50, 5, randomPalette(randint(2, 5)));
  10. })
复制代码

1.2 锦缎效果 (Brocade)
typescript
  1. new effects.ParticleEffect(600, 500, function (anchor, particlesPerSecond) {
  2.     class BrocadeFactory extends particles.SprayFactory {
  3.         // 创建"伞状"图案,25%概率使用主色,75%概率使用副色
  4.         createParticle(anchor) {
  5.             if (this.galois.percentChance(25)) {
  6.                 p.color = this.palette[0];
  7.                 p.lifespan = randint(50, 150);
  8.             } else {
  9.                 p.color = this.palette[1];
  10.                 p.lifespan = randint(50, 350);
  11.             }
  12.         }
  13.     }
  14. })
复制代码

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

1.4 十字效果 (Crossette)
typescript
  1. createEffect(100, 600, () => {
  2.     class CrossetteFactory extends particles.SprayFactory {
  3.         // 创建直线飞行带小分支的效果
  4.         drawParticle(p, x, y) {
  5.             // 绘制双线使粒子位置更粗
  6.             screen.drawLine(x, y, this.anchor.x, this.anchor.y, p.color);
  7.         }
  8.     }
  9. })
复制代码

2. 辅助函数
createEffect 包装器
typescript
  1. function createEffect(defaultParticlesPerSecond, defaultLifespan, factoryFactory) {
  2.     // 将简单粒子工厂包装成易于使用的效果
  3.     return new effects.ParticleEffect(defaultParticlesPerSecond, defaultLifespan,
  4.         (anchor, pps) => new particles.ParticleSource(anchor, pps, factoryFactory()));
  5. }
复制代码

3. 背景效果
不闪烁的星星
typescript
  1. new effects.ScreenEffect(2, 5, 5000, function (anchor, particlesPerSecond) {
  2.     class NoTwinkleStarFactory extends particles.StarFactory {
  3.         // 覆盖draw方法,创建不闪烁的星星
  4.         drawParticle(p, x, y) {
  5.             const selected = this.images[rest & p.data].clone();
  6.             selected.replace(0x1, p.color);
  7.             screen.drawTransparentImage(selected, x, y);
  8.         }
  9.     }
  10. }).startScreenEffect();
复制代码

4. 烟花轨迹效果
typescript
  1. const fireworkTrail = createEffect(25, 50, a => {
  2.     class FireworkTrail extends particles.ParticleFactory {
  3.         createParticle(anchor) {
  4.             p.vx = Fx.neg(Fx8(anchor.vx + randint(-10, 10))); // 反向速度加随机偏移
  5.             p.vy = Fx.neg(Fx8(anchor.vy >> 1)); // 垂直速度减半并反向
  6.             p.color = Math.percentChance(90) ? 0xE : randint(0x1, 0xD); // 90%白色,10%随机色
  7.         }
  8.     }
  9. });
复制代码

5. 发射控制
按钮控制
typescript
  1. controller.anyButton.onEvent(ControllerButtonEvent.Pressed, tryToFire);
  2. function tryToFire() {
  3.     if (lastFired + TIMEOUT < time) {
  4.         const firework = sprites.createProjectileFromSide(img`e e`, vx, randint(-150, -125));
  5.         firework.startEffect(fireworkTrail);
  6.         firework.ay = 100; // 重力加速度
  7.         firework.lifespan = randint(800, 1200); // 生存时间
  8.     }
  9. }
复制代码

自动发射
typescript
  1. game.onUpdate(function () {
  2.     if (lastFired + (3 * TIMEOUT) < game.runtime()) {
  3.         tryToFire(); // 如果一段时间没有发射,自动发射
  4.     }
  5. });
复制代码

6. **效果
typescript
  1. sprites.onDestroyed(SpriteKind.Projectile, s => {
  2.     Math.pickRandom(fireworkEffects).start(s, 500); // 烟花销毁时随机选择一种**效果
  3. });
复制代码

7. 颜色系统
调色板设置
typescript
  1. const p = color.currentPalette();
  2. p.setColor(0xB, generatePastel().hexValue());
  3. p.setColor(0xC, generatePastel().hexValue());
  4. p.setColor(0xD, generatePastel().hexValue());
  5. p.setColor(0xE, 0xFFFFFF); // 白色
  6. color.setPalette(p);
复制代码

颜色渐变
typescript
  1. forever(() => {
  2.     new color.Fade()
  3.         .mapEndHSL(generatePastel, 0x1, 0xA) // 在颜色1-10之间渐变
  4.         .startUntilDone(500);
  5. })
  6. function generatePastel() {
  7.     return new color.HSL(
  8.         randint(0, 359), // 随机色相
  9.         1,               // 100%饱和度
  10.         randint(75, 95) / 100 // 75-95%亮度(pastel色调)
  11.     );
  12. }
复制代码

随机调色板生成
typescript
  1. function randomPalette(len: number) {
  2.     const palette: number[] = [];
  3.     for (let i = 0; i < len; i++) {
  4.         const selected = randint(1, 0xA); // 从颜色1-10中随机选择
  5.         if (palette.indexOf(selected) < 0) { // 确保不重复
  6.             palette.push(selected);
  7.         }
  8.     }
  9.     return palette;
  10. }
复制代码

技术特点
高级粒子系统:使用MakeCode的粒子系统创建复杂效果
面向对象设计:通过类继承创建自定义粒子工厂
颜色管理:动态调色板和HSL颜色空间使用
物理模拟:模拟重力、速度、加速度等物理效果
随机化:大量使用随机数创建多样化效果
性能优化:通过粒子生命周期管理优化性能

视觉效果
多种烟花类型:4种不同的**效果
颜色渐变:背景颜色持续渐变
粒子多样性:不同形状、颜色、寿命的粒子
轨迹效果:烟花上升时的尾迹
星空背景:不闪烁的星星背景

回复

使用道具 举报

驴友花雕  中级技神
 楼主|

发表于 4 小时前

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

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

【花雕动手做】基于 Kitronik 可编程开发板之漫天烟花图1

实验场景记录

【花雕动手做】基于 Kitronik 可编程开发板之漫天烟花图3

【花雕动手做】基于 Kitronik 可编程开发板之漫天烟花图2

回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

为本项目制作心愿单
购买心愿单
心愿单 编辑
[[wsData.name]]

硬件清单

  • [[d.name]]
btnicon
我也要做!
点击进入购买页面
上海智位机器人股份有限公司 沪ICP备09038501号-4 备案 沪公网安备31011502402448

© 2013-2025 Comsenz Inc. Powered by Discuz! X3.4 Licensed

mail