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

[项目] 【花雕动手做】基于Kitronik可编程开发板之动画沙子游戏

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

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

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

【花雕动手做】基于Kitronik可编程开发板之动画沙子游戏图2

【花雕动手做】基于Kitronik可编程开发板之动画沙子游戏图1

驴友花雕  中级技神
 楼主|

发表于 5 小时前

【花雕动手做】基于Kitronik可编程开发板之动画沙子游戏

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

JavaScript 实验代码

  1. // Animated LED sand ported from
  2. // https://learn.adafruit.com/animated-led-sand/code
  3. const N_GRAINS = 80;
  4. const grainImg = img`
  5.     b b b b b b b .
  6.     b d d d d d b .
  7.     b d d d d d b c
  8.     b d d d d d b c
  9.     b d d d d d b c
  10.     b d d d d d b c
  11.     b b b b b b b c
  12.     . . c c c c c c
  13. `;
  14. const GRAIN_RADIUS = grainImg.width;
  15. const WIDTH = Math.idiv(screen.width, GRAIN_RADIUS); // Display width in pixels
  16. const HEIGHT = Math.idiv(screen.height, GRAIN_RADIUS); // Display height in pixels
  17. // The 'sand' grains exist in an integer coordinate space that's 256X
  18. // the scale of the pixel grid, allowing them to move and interact at
  19. // less than whole-pixel increments.
  20. const MAX_X = (WIDTH * 256 - 1); // Maximum X coordinate in grain space
  21. const MAX_Y = (HEIGHT * 256 - 1); // Maximum Y coordinate
  22. class Grain {
  23.     constructor(public x: number, public y: number, public vx: number, public vy: number)
  24.     { }
  25. }
  26. const grain: Grain[] = [];
  27. const imgbuf = control.createBuffer(WIDTH * HEIGHT);
  28. // SETUP - RUNS ONCE AT PROGRAM START --------------------------------------
  29. function setup() {
  30.     for (let i = 0; i < N_GRAINS; i++) {  // For each sand grain...
  31.         grain.push(new Grain(0, 0, 0, 0));
  32.         let j = 0;
  33.         do {
  34.             grain[i].x = randint(0, (WIDTH << 8) - 1); // Assign random position within
  35.             grain[i].y = randint(0, (HEIGHT << 8) - 1); // the 'grain' coordinate space
  36.             // Check if corresponding pixel position is already occupied...
  37.             for (j = 0; (j < i) && (((grain[i].x >> 8) != (grain[j].x >> 8)) ||
  38.                 ((grain[i].y >> 8) != (grain[j].y >> 8))); j++);
  39.         } while (j < i); // Keep retrying until a clear spot is found
  40.         imgbuf[(grain[i].y >> 8) * WIDTH + (grain[i].x >> 8)] = 0xff; // Mark it
  41.         grain[i].vx = grain[i].vy = 0; // Initial velocity is zero
  42.     }
  43. }
  44. setup();
  45. // MAIN LOOP - RUNS ONCE PER FRAME OF ANIMATION ----------------------------
  46. game.onUpdate(function () {
  47.     // Limit the animation frame rate to MAX_FPS.  Because the subsequent sand
  48.     // calculations are non-deterministic (don't always take the same amount
  49.     // of time, depending on their current states), this helps ensure that
  50.     // things like gravity appear constant in the simulation.
  51.     // Read accelerometer...
  52.     let ax = controller.acceleration(ControllerDimension.X) >> 8;
  53.     let ay = -controller.acceleration(ControllerDimension.Y) >> 8;
  54.     let az = Math.idiv(Math.abs(controller.acceleration(ControllerDimension.Z)), 2048);
  55.     az = (az >= 3) ? 1 : 4 - az;      // Clip & invert
  56.     ax -= az;                         // Subtract motion factor from X, Y
  57.     ay -= az;
  58.     let az2 = az * 2 + 1;         // Range of random motion to add back in
  59.     // ...and apply 2D accel vector to grain velocities...
  60.     let v2; // Velocity squared
  61.     let v;  // Absolute velocity
  62.     for (let i = 0; i < N_GRAINS; i++) {
  63.         const graini = grain[i];
  64.         graini.vx += ax + randint(0, az2); // A little randomness makes
  65.         graini.vy += ay + randint(0, az2); // tall stacks topple better!
  66.         // Terminal velocity (in any direction) is 256 units -- equal to
  67.         // 1 pixel -- which keeps moving grains from passing through each other
  68.         // and other such mayhem.  Though it takes some extra math, velocity is
  69.         // clipped as a 2D vector (not separately-limited X & Y) so that
  70.         // diagonal movement isn't faster
  71.         v2 = graini.vx * graini.vx + graini.vy * graini.vy;
  72.         if (v2 > 65536) { // If v^2 > 65536, then v > 256
  73.             //v = Math.sqrt(v2) | 0; // Velocity vector magnitude
  74.             // sqrt expensive on hw
  75.             v = Math.max(graini.vx, graini.vy);
  76.             graini.vx = Math.idiv(graini.vx, v) >> 8; // Maintain heading
  77.             graini.vy = Math.idiv(graini.vy, v) >> 8; // Limit magnitude
  78.         }
  79.     }
  80.     // ...then update position of each grain, one at a time, checking for
  81.     // collisions and having them react.  This really seems like it shouldn't
  82.     // work, as only one grain is considered at a time while the rest are
  83.     // regarded as stationary.  Yet this naive algorithm, taking many not-
  84.     // technically-quite-correct steps, and repeated quickly enough,
  85.     // visually integrates into something that somewhat resembles physics.
  86.     // (I'd initially tried implementing this as a bunch of concurrent and
  87.     // "realistic" elastic collisions among circular grains, but the
  88.     // calculations and volument of code quickly got out of hand for both
  89.     // the tiny 8-bit AVR microcontroller and my tiny dinosaur brain.)
  90.     for (let i = 0; i < N_GRAINS; i++) {
  91.         const graini = grain[i];
  92.         let newx = graini.x + graini.vx; // New position in grain space
  93.         let newy = graini.y + graini.vy;
  94.         if (newx > MAX_X) {               // If grain would go out of bounds
  95.             newx = MAX_X;          // keep it inside, and
  96.             graini.vx = - graini.vx >> 1;             // give a slight bounce off the wall
  97.         } else if (newx < 0) {
  98.             newx = 0;
  99.             graini.vx = - graini.vx >> 1;
  100.         }
  101.         if (newy > MAX_Y) {
  102.             newy = MAX_Y;
  103.             graini.vy = - graini.vy >> 1;
  104.         } else if (newy < 0) {
  105.             newy = 0;
  106.             graini.vy = - graini.vy >> 1;
  107.         }
  108.         let oldidx = (graini.y >> 8) * WIDTH + (graini.x >> 8); // Prior pixel #
  109.         let newidx = (newy >> 8) * WIDTH + (newx >> 8); // New pixel #
  110.         if ((oldidx != newidx) && // If grain is moving to a new pixel...
  111.             imgbuf[newidx]) {       // but if that pixel is already occupied...
  112.             let delta = Math.abs(newidx - oldidx); // What direction when blocked?
  113.             if (delta == 1) {            // 1 pixel left or right)
  114.                 newx = graini.x;  // Cancel X motion
  115.                 graini.vx = -graini.vx >> 1;          // and bounce X velocity (Y is OK)
  116.                 newidx = oldidx;      // No pixel change
  117.             } else if (delta == WIDTH) { // 1 pixel up or down
  118.                 newy = graini.y;  // Cancel Y motion
  119.                 graini.vy = -graini.vy >> 1;          // and bounce Y velocity (X is OK)
  120.                 newidx = oldidx;      // No pixel change
  121.             } else { // Diagonal intersection is more tricky...
  122.                 // Try skidding along just one axis of motion if possible (start w/
  123.                 // faster axis).  Because we've already established that diagonal
  124.                 // (both-axis) motion is occurring, moving on either axis alone WILL
  125.                 // change the pixel index, no need to check that again.
  126.                 if ((Math.abs(graini.vx) - Math.abs(graini.vy)) >= 0) { // X axis is faster
  127.                     newidx = (graini.y >> 8) * WIDTH + (newx >> 8);
  128.                     if (!imgbuf[newidx]) { // That pixel's free!  Take it!  But...
  129.                         newy = graini.y; // Cancel Y motion
  130.                         graini.vy = - graini.vy >> 1;         // and bounce Y velocity
  131.                     } else { // X pixel is taken, so try Y...
  132.                         newidx = (newy >> 8) * WIDTH + (graini.x >> 8);
  133.                         if (!imgbuf[newidx]) { // Pixel is free, take it, but first...
  134.                             newx = graini.x; // Cancel X motion
  135.                             graini.vx = - graini.vx >> 1;         // and bounce X velocity
  136.                         } else { // Both spots are occupied
  137.                             newx = graini.x; // Cancel X & Y motion
  138.                             newy = graini.y;
  139.                             graini.vx = - graini.vx >> 1;         // Bounce X & Y velocity
  140.                             graini.vy /= - graini.vy >> 1;
  141.                             newidx = oldidx;     // Not moving
  142.                         }
  143.                     }
  144.                 } else { // Y axis is faster, start there
  145.                     newidx = (newy >> 8) * WIDTH + (graini.x >> 8);
  146.                     if (!imgbuf[newidx]) { // Pixel's free!  Take it!  But...
  147.                         newx = graini.x; // Cancel X motion
  148.                         graini.vy = - graini.vy >> 1;         // and bounce X velocity
  149.                     } else { // Y pixel is taken, so try X...
  150.                         newidx = (graini.y >> 8) * WIDTH + (newx >> 8);
  151.                         if (!imgbuf[newidx]) { // Pixel is free, take it, but first...
  152.                             newy = graini.y; // Cancel Y motion
  153.                             graini.vy = - graini.vy >> 1;         // and bounce Y velocity
  154.                         } else { // Both spots are occupied
  155.                             newx = graini.x; // Cancel X & Y motion
  156.                             newy = graini.y;
  157.                             graini.vx = - graini.vx >> 1;         // Bounce X & Y velocity
  158.                             graini.vy = - graini.vy >> 1;
  159.                             newidx = oldidx;     // Not moving
  160.                         }
  161.                     }
  162.                 }
  163.             }
  164.         }
  165.         graini.x = newx; // Update grain position
  166.         graini.y = newy;
  167.         imgbuf[oldidx] = 0;    // Clear old spot (might be same as new, that's OK)
  168.         imgbuf[newidx] = 0xff;  // Set new spot
  169.     }
  170. });
  171. game.onPaint(function () {
  172.     for (let x = 0; x < WIDTH; ++x) {
  173.         const xs = x * GRAIN_RADIUS;
  174.         for (let y = 0; y < HEIGHT; ++y) {
  175.             const ys = y * GRAIN_RADIUS;
  176.             if (imgbuf[y * WIDTH + x])
  177.                 screen.drawImage(grainImg, xs, ys)
  178.         }
  179.     }
  180. })
复制代码


回复

使用道具 举报

驴友花雕  中级技神
 楼主|

发表于 5 小时前

【花雕动手做】基于Kitronik可编程开发板之动画沙子游戏

ARCADE MakeCode 之动画沙子游戏代码解读
这是一个模拟沙子物理效果的动画程序,使用加速度计控制沙粒的运动。

代码结构分析

1. 常量和全局变量定义
javascript
  1. const N_GRAINS = 80; // 沙粒数量
  2. const grainImg = img`...`; // 单个沙粒的图像
  3. const GRAIN_RADIUS = grainImg.width; // 沙粒半径
  4. const WIDTH = Math.idiv(screen.width, GRAIN_RADIUS); // 屏幕宽度(以沙粒为单位)
  5. const HEIGHT = Math.idiv(screen.height, GRAIN_RADIUS); // 屏幕高度(以沙粒为单位)
  6. // 沙粒坐标空间是像素网格的256倍,允许亚像素精度的移动
  7. const MAX_X = (WIDTH * 256 - 1); // 沙粒空间中的最大X坐标
  8. const MAX_Y = (HEIGHT * 256 - 1); // 沙粒空间中的最大Y坐标
复制代码


2. 沙粒类定义
javascript
  1. class Grain {
  2.     constructor(public x: number, public y: number, public vx: number, public vy: number)
  3.     { }
  4. }
复制代码

每个沙粒对象包含位置(x,y)和速度(vx,vy)信息。

3. 全局数据结构
javascript
  1. const grain: Grain[] = []; // 沙粒对象数组
  2. const imgbuf = control.createBuffer(WIDTH * HEIGHT); // 缓冲区,记录每个像素位置是否有沙粒
复制代码


4. 初始化函数
javascript
  1. function setup() {
  2.     for (let i = 0; i < N_GRAINS; i++) {  // 为每个沙粒...
  3.         grain.push(new Grain(0, 0, 0, 0));
  4.         let j = 0;
  5.         do {
  6.             // 在"沙粒"坐标空间内分配随机位置
  7.             grain[i].x = randint(0, (WIDTH << 8) - 1);
  8.             grain[i].y = randint(0, (HEIGHT << 8) - 1);
  9.             
  10.             // 检查对应的像素位置是否已被占用...
  11.             for (j = 0; (j < i) && (((grain[i].x >> 8) != (grain[j].x >> 8)) ||
  12.                 ((grain[i].y >> 8) != (grain[j].y >> 8))); j++);
  13.         } while (j < i); // 不断重试直到找到空位
  14.         
  15.         imgbuf[(grain[i].y >> 8) * WIDTH + (grain[i].x >> 8)] = 0xff; // 标记位置
  16.         grain[i].vx = grain[i].vy = 0; // 初始速度为零
  17.     }
  18. }
  19. setup(); // 执行初始化
复制代码


5. 主更新循环
javascript
  1. game.onUpdate(function () {
  2.     // 读取加速度计数据...
  3.     let ax = controller.acceleration(ControllerDimension.X) >> 8;
  4.     let ay = -controller.acceleration(ControllerDimension.Y) >> 8;
  5.     let az = Math.idiv(Math.abs(controller.acceleration(ControllerDimension.Z)), 2048);
  6.     az = (az >= 3) ? 1 : 4 - az;      // 裁剪和反转
  7.     ax -= az;                         // 从X、Y中减去运动因子
  8.     ay -= az;
  9.     let az2 = az * 2 + 1;         // 要添加回的随机运动范围
  10.     // 将2D加速度向量应用于沙粒速度...
  11.     let v2; // 速度平方
  12.     let v;  // 绝对速度
  13.     for (let i = 0; i < N_GRAINS; i++) {
  14.         const graini = grain[i];
  15.         graini.vx += ax + randint(0, az2); // 一点随机性使
  16.         graini.vy += ay + randint(0, az2); // 高堆更容易倒塌!
  17.         
  18.         // 限制终端速度(任何方向)为256单位 - 等于1像素
  19.         v2 = graini.vx * graini.vx + graini.vy * graini.vy;
  20.         if (v2 > 65536) { // 如果v^2 > 65536,则v > 256
  21.             // 在硬件上sqrt很昂贵,所以使用近似值
  22.             v = Math.max(graini.vx, graini.vy);
  23.             graini.vx = Math.idiv(graini.vx, v) >> 8; // 保持方向
  24.             graini.vy = Math.idiv(graini.vy, v) >> 8; // 限制大小
  25.         }
  26.     }
  27.     // 更新每个沙粒的位置,检查碰撞并做出反应
  28.     for (let i = 0; i < N_GRAINS; i++) {
  29.         const graini = grain[i];
  30.         let newx = graini.x + graini.vx; // 沙粒空间中的新位置
  31.         let newy = graini.y + graini.vy;
  32.         
  33.         // 边界检查
  34.         if (newx > MAX_X) {
  35.             newx = MAX_X;
  36.             graini.vx = - graini.vx >> 1; // 轻微反弹
  37.         } else if (newx < 0) {
  38.             newx = 0;
  39.             graini.vx = - graini.vx >> 1;
  40.         }
  41.         if (newy > MAX_Y) {
  42.             newy = MAX_Y;
  43.             graini.vy = - graini.vy >> 1;
  44.         } else if (newy < 0) {
  45.             newy = 0;
  46.             graini.vy = - graini.vy >> 1;
  47.         }
  48.         let oldidx = (graini.y >> 8) * WIDTH + (graini.x >> 8); // 先前像素索引
  49.         let newidx = (newy >> 8) * WIDTH + (newx >> 8); // 新像素索引
  50.         
  51.         // 碰撞检测和处理
  52.         if ((oldidx != newidx) && // 如果沙粒移动到新像素...
  53.             imgbuf[newidx]) {       // 但该像素已被占用...
  54.             let delta = Math.abs(newidx - oldidx); // 被阻挡时的方向?
  55.             if (delta == 1) {            // 左右1像素
  56.                 newx = graini.x;  // 取消X运动
  57.                 graini.vx = -graini.vx >> 1;          // 反弹X速度(Y可以)
  58.                 newidx = oldidx;      // 无像素变化
  59.             } else if (delta == WIDTH) { // 上下1像素
  60.                 newy = graini.y;  // 取消Y运动
  61.                 graini.vy = -graini.vy >> 1;          // 反弹Y速度(X可以)
  62.                 newidx = oldidx;      // 无像素变化
  63.             } else { // 对角线交叉更复杂...
  64.                 // 尝试沿一个运动轴滑动(从较快的轴开始)
  65.                 if ((Math.abs(graini.vx) - Math.abs(graini.vy)) >= 0) { // X轴更快
  66.                     newidx = (graini.y >> 8) * WIDTH + (newx >> 8);
  67.                     if (!imgbuf[newidx]) { // 该像素空闲!占据它!
  68.                         newy = graini.y; // 取消Y运动
  69.                         graini.vy = - graini.vy >> 1;         // 反弹Y速度
  70.                     } else { // X像素被占用,尝试Y...
  71.                         newidx = (newy >> 8) * WIDTH + (graini.x >> 8);
  72.                         if (!imgbuf[newidx]) { // 像素空闲,占据它
  73.                             newx = graini.x; // 取消X运动
  74.                             graini.vx = - graini.vx >> 1;         // 反弹X速度
  75.                         } else { // 两个位置都被占用
  76.                             newx = graini.x; // 取消X和Y运动
  77.                             newy = graini.y;
  78.                             graini.vx = - graini.vx >> 1;         // 反弹X和Y速度
  79.                             graini.vy = - graini.vy >> 1;
  80.                             newidx = oldidx;     // 不移动
  81.                         }
  82.                     }
  83.                 } else { // Y轴更快,从那里开始
  84.                     newidx = (newy >> 8) * WIDTH + (graini.x >> 8);
  85.                     if (!imgbuf[newidx]) { // 像素空闲!占据它!
  86.                         newx = graini.x; // 取消X运动
  87.                         graini.vy = - graini.vy >> 1;         // 反弹X速度
  88.                     } else { // Y像素被占用,尝试X...
  89.                         newidx = (graini.y >> 8) * WIDTH + (newx >> 8);
  90.                         if (!imgbuf[newidx]) { // 像素空闲,占据它
  91.                             newy = graini.y; // 取消Y运动
  92.                             graini.vy = - graini.vy >> 1;         // 反弹Y速度
  93.                         } else { // 两个位置都被占用
  94.                             newx = graini.x; // 取消X和Y运动
  95.                             newy = graini.y;
  96.                             graini.vx = - graini.vx >> 1;         // 反弹X和Y速度
  97.                             graini.vy = - graini.vy >> 1;
  98.                             newidx = oldidx;     // 不移动
  99.                         }
  100.                     }
  101.                 }
  102.             }
  103.         }
  104.         
  105.         // 更新沙粒位置和缓冲区
  106.         graini.x = newx;
  107.         graini.y = newy;
  108.         imgbuf[oldidx] = 0;    // 清除旧位置
  109.         imgbuf[newidx] = 0xff;  // 设置新位置
  110.     }
  111. });
复制代码


6. 渲染函数
javascript
  1. game.onPaint(function () {
  2.     for (let x = 0; x < WIDTH; ++x) {
  3.         const xs = x * GRAIN_RADIUS;
  4.         for (let y = 0; y < HEIGHT; ++y) {
  5.             const ys = y * GRAIN_RADIUS;
  6.             if (imgbuf[y * WIDTH + x]) // 如果该位置有沙粒
  7.                 screen.drawImage(grainImg, xs, ys) // 绘制沙粒图像
  8.         }
  9.     }
  10. })
复制代码

技术要点解析
亚像素精度:沙粒在256倍于像素网格的坐标空间中移动,实现平滑的亚像素运动
碰撞检测:使用缓冲区(imgbuf)记录每个像素位置是否有沙粒,实现高效的碰撞检测

物理模拟:
加速度计数据影响沙粒运动
速度限制防止沙粒相互穿透
碰撞时速度反弹并衰减

性能优化:
使用近似计算代替昂贵的平方根运算
使用缓冲区而不是直接像素操作提高性能
简化的物理模型在视觉上仍然逼真。

回复

使用道具 举报

驴友花雕  中级技神
 楼主|

发表于 5 小时前

【花雕动手做】基于Kitronik可编程开发板之动画沙子游戏

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

【花雕动手做】基于Kitronik可编程开发板之动画沙子游戏图3

实验场景记录

【花雕动手做】基于Kitronik可编程开发板之动画沙子游戏图2

【花雕动手做】基于Kitronik可编程开发板之动画沙子游戏图1

回复

使用道具 举报

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

本版积分规则

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

硬件清单

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

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

mail