驴友花雕 发表于 2025-9-25 09:45:25

【花雕动手做】基于 Kitronik 游戏机开发板之乒乓球游戏



Kitronik ARCADE 使用 Microsoft MakeCode 平台,具有以下优势:
图形化编程界面:适合初学者,支持拖拽式编程。
即时模拟器:可以实时测试游戏效果。
硬件兼容性:可部署到 Kitronik ARCADE 设备,实现实体游戏体验。
支持 Python/JavaScript:便于进阶学习。



驴友花雕 发表于 2025-9-25 09:47:17

【花雕动手做】基于 Kitronik 游戏机开发板之乒乓球游戏

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

JavaScript 实验代码

const BALL_IMAGE = img`
    . . e e 1 e e e . .
    . e 1 1 d d d d e .
    e 1 d d d d d d d e
    e d d d d d d d d e
    e d d d d d d d d e
    e d d d d d d d d e
    e d d d d d d d d e
    . e d d d d d d e .
    . . e e e e e e . .
`;

const PADDLE_SPEED = 150;
const PADDING_FROM_WALL = 3;
let pingMessage = false;

// if player doesn't interact for 'TIMEOUT' time, revert to ai
const TIMEOUT = 5000;
let playerOneLastMove = -TIMEOUT;
let playerTwoLastMove = -TIMEOUT;

controller.setRepeatDefault(0, 1000);

controller.up.onEvent(ControllerButtonEvent.Repeated, () => playerOneLastMove = game.runtime());
controller.down.onEvent(ControllerButtonEvent.Repeated, () => playerOneLastMove = game.runtime());
controller.player2.up.onEvent(ControllerButtonEvent.Repeated, () => playerTwoLastMove = game.runtime());
controller.player2.down.onEvent(ControllerButtonEvent.Repeated, () => playerTwoLastMove = game.runtime());

const playerOne = createPlayer(info.player1);
playerOne.left = PADDING_FROM_WALL;
controller.moveSprite(playerOne, 0, PADDLE_SPEED);

const playerTwo = createPlayer(info.player2);
playerTwo.right = screen.width - PADDING_FROM_WALL;
controller.player2.moveSprite(playerTwo, 0, PADDLE_SPEED);

createBall();

function createPlayer(player: info.PlayerInfo) {
    const output = sprites.create(image.create(3, 18), SpriteKind.Player);

    output.image.fill(player.bg);
    output.setStayInScreen(true);

    player.setScore(0);
    player.showPlayer = false;

    return output;
}

function createBall() {
    let ball = sprites.create(BALL_IMAGE.clone(), SpriteKind.Enemy);
    ball.vy = randint(-20, 20);
    ball.vx = 60 * (Math.percentChance(50) ? 1 : -1);
}

game.onUpdate(function () {
    sprites
      .allOfKind(SpriteKind.Enemy)
      .forEach(b => {
            const scoreRight = b.x < 0;
            const scoreLeft = b.x >= screen.width;

            if (scoreRight) {
                info.player2.changeScoreBy(1)
            } else if (scoreLeft) {
                info.player1.changeScoreBy(1)
            }

            if (b.top < 0) {
                b.vy = Math.abs(b.vy);
            } else if (b.bottom > screen.height) {
                b.vy = -Math.abs(b.vy);
            }

            if (scoreLeft || scoreRight) {
                b.destroy(effects.disintegrate, 500);
                control.runInParallel(function () {
                  pause(250);
                  createBall();
                });
            }
      }
      );
});

game.onShade(function () {
    if (pingMessage) {
      screen.printCenter("ping", 5);
    } else {
      screen.printCenter("pong", 5);
    }
})

sprites.onOverlap(SpriteKind.Player, SpriteKind.Enemy,
    (sprite: Sprite, otherSprite: Sprite) => {
      const fromCenter = otherSprite.y - sprite.y;

      otherSprite.vx = otherSprite.vx * -1.05;
      otherSprite.vy += (sprite.vy >> 1) + (fromCenter * 3);

      otherSprite.startEffect(effects.ashes, 150);
      sprite.startEffect(effects.ashes, 100);

      otherSprite.image.setPixel(
            randint(1, otherSprite.image.width - 2),
            randint(1, otherSprite.image.height - 2),
            sprite.image.getPixel(0, 0)
      );

      pingMessage = !pingMessage;

      // time out this event so it doesn't retrigger on the same collision
      pause(500);
    }
);

controller.A.onEvent(ControllerButtonEvent.Pressed, () => addBall(info.player1));
controller.B.onEvent(ControllerButtonEvent.Pressed, () => removeBall(info.player1));
controller.player2.A.onEvent(ControllerButtonEvent.Pressed, () => addBall(info.player2));
controller.player2.B.onEvent(ControllerButtonEvent.Pressed, () => removeBall(info.player2));

function addBall(player: info.PlayerInfo) {
    player.changeScoreBy(-2);
    createBall();
}

function removeBall(player: info.PlayerInfo) {
    const balls = sprites.allOfKind(SpriteKind.Enemy);
    if (balls.length > 1) {
      Math.pickRandom(balls).destroy();
      player.changeScoreBy(-2);
    }
}

game.onUpdate(function () {
    const currTime = game.runtime();

    if (playerOneLastMove + TIMEOUT < currTime) {
      trackBall(playerOne);
    }

    if (playerTwoLastMove + TIMEOUT < currTime) {
      trackBall(playerTwo);
    }

    function trackBall(player: Sprite) {
      const next = nextBall(player);
      if (!next)
            return;
      if (ballFacingPlayer(player, next)) {
            // move to where ball is expected to intersect
            intersectBall(player, next);
      } else {
            // relax, ball is going other way
            player.vy = 0;
      }
    }

    function nextBall(player: Sprite) {
      return sprites
            .allOfKind(SpriteKind.Enemy)
            .sort((a, b) => {
                const aFacingPlayer = ballFacingPlayer(player, a);
                const bFacingPlayer = ballFacingPlayer(player, b);

                // else prefer ball facing player
                if (aFacingPlayer && !bFacingPlayer) return -1;
                else if (!aFacingPlayer && bFacingPlayer) return 1;

                // else prefer ball that will next reach player
                const aDiff = Math.abs((a.x - player.x) / a.vx);
                const bDiff = Math.abs((b.x - player.x) / b.vx);
                return aDiff - bDiff;
            });
    }

    function ballFacingPlayer(player: Sprite, ball: Sprite) {
      return (ball.vx < 0 && player.x < 80) || (ball.vx > 0 && player.x > 80);
    }

    function intersectBall(player: Sprite, target: Sprite) {
      const projectedDY = (target.x - player.x) * target.vy / target.vx;
      let intersectionPoint = target.y - projectedDY;

      // quick 'estimation' for vertical bounces
      if (intersectionPoint < 0) {
            intersectionPoint = Math.abs(intersectionPoint % screen.height)
      } else if (intersectionPoint > screen.height) {
            intersectionPoint -= intersectionPoint % screen.height;
      }

      // move toward estimated intersection point if not in range
      if (intersectionPoint > player.y + (player.height >> 2)) {
            player.vy = PADDLE_SPEED;
      } else if (intersectionPoint < player.y - (player.height >> 2)) {
            player.vy = -PADDLE_SPEED;
      } else {
            player.vy = 0;
      }
    }
});

驴友花雕 发表于 2025-9-25 09:52:30

【花雕动手做】基于 Kitronik 游戏机开发板之乒乓球游戏

这是一个功能丰富的双人乒乓球游戏,支持玩家对战和AI自动对战,包含多种高级特性如多球模式、智能AI追踪等。
代码结构分析

1. 常量定义和初始化
javascript
const BALL_IMAGE = img`...`;// 球的像素图像

const PADDLE_SPEED = 150;   // 球拍移动速度

const PADDING_FROM_WALL = 3;// 球拍离墙的距离

const TIMEOUT = 5000;         // AI接管超时时间(5秒)

2. 玩家交互检测系统
javascript
let playerOneLastMove = -TIMEOUT;

let playerTwoLastMove = -TIMEOUT;



// 设置按键重复延迟

controller.setRepeatDefault(0, 1000);



// 监听玩家操作时间戳

controller.up.onEvent(ControllerButtonEvent.Repeated, () => playerOneLastMove = game.runtime());

controller.down.onEvent(ControllerButtonEvent.Repeated, () => playerOneLastMove = game.runtime());
智能特性:如果玩家5秒内没有操作,AI会自动接管控制。

3. 游戏对象创建
创建玩家球拍
javascript
function createPlayer(player: info.PlayerInfo) {

    const output = sprites.create(image.create(3, 18), SpriteKind.Player);

    output.image.fill(player.bg);// 使用玩家主题色

    output.setStayInScreen(true);// 限制在屏幕内

    return output;

}
创建乒乓球
javascript
function createBall() {

    let ball = sprites.create(BALL_IMAGE.clone(), SpriteKind.Enemy);

    ball.vy = randint(-20, 20);    // 随机垂直速度

    ball.vx = 60 * (Math.percentChance(50) ? 1 : -1);// 随机水平方向

}

4. 核心游戏逻辑
球的状态更新
javascript
game.onUpdate(function () {

    sprites.allOfKind(SpriteKind.Enemy).forEach(b => {

      // 检测得分

      const scoreRight = b.x < 0;

      const scoreLeft = b.x >= screen.width;

      

      if (scoreRight) info.player2.changeScoreBy(1);

      else if (scoreLeft) info.player1.changeScoreBy(1);

      

      // 上下边界反弹

      if (b.top < 0) b.vy = Math.abs(b.vy);

      else if (b.bottom > screen.height) b.vy = -Math.abs(b.vy);

      

      // 得分后重新生成球

      if (scoreLeft || scoreRight) {

            b.destroy(effects.disintegrate, 500);

            control.runInParallel(() => {

                pause(250);

                createBall();

            });

      }

    });

});
碰撞检测与物理响应
javascript
sprites.onOverlap(SpriteKind.Player, SpriteKind.Enemy, (sprite, otherSprite) => {

    const fromCenter = otherSprite.y - sprite.y;// 计算击中点偏移

   

    // 物理反弹效果

    otherSprite.vx = otherSprite.vx * -1.05;      // 反向并加速5%

    otherSprite.vy += (sprite.vy >> 1) + (fromCenter * 3);// 加入旋转效果

   

    // 视觉效果

    otherSprite.startEffect(effects.ashes, 150);

    sprite.startEffect(effects.ashes, 100);

   

    // 球的颜色变化(击中时染色)

    otherSprite.image.setPixel(

      randint(1, otherSprite.image.width - 2),

      randint(1, otherSprite.image.height - 2),

      sprite.image.getPixel(0, 0)// 使用球拍颜色

    );

   

    pingMessage = !pingMessage;// 切换"ping"/"pong"显示

    pause(500);// 防重复触发

});

5. 高级AI追踪系统
这是游戏最复杂和智能的部分:

javascript
function trackBall(player: Sprite) {

    const next = nextBall(player);

    if (!next) return;

   

    if (ballFacingPlayer(player, next)) {

      intersectBall(player, next);// 追踪球的预计落点

    } else {

      player.vy = 0;// 球朝反方向,放松等待

    }

}
智能球选择算法
javascript
function nextBall(player: Sprite) {

    return sprites.allOfKind(SpriteKind.Enemy).sort((a, b) => {

      const aFacingPlayer = ballFacingPlayer(player, a);

      const bFacingPlayer = ballFacingPlayer(player, b);

      

      // 优先选择面向玩家的球

      if (aFacingPlayer && !bFacingPlayer) return -1;

      else if (!aFacingPlayer && bFacingPlayer) return 1;

      

      // 其次选择最先到达的球

      const aDiff = Math.abs((a.x - player.x) / a.vx);

      const bDiff = Math.abs((b.x - player.x) / b.vx);

      return aDiff - bDiff;

    });

}
物理轨迹预测算法
javascript
function intersectBall(player: Sprite, target: Sprite) {

    // 计算球的预计落点:使用相似三角形原理

    const projectedDY = (target.x - player.x) * target.vy / target.vx;

    let intersectionPoint = target.y - projectedDY;

   

    // 处理边界反弹的估算

    if (intersectionPoint < 0) {

      intersectionPoint = Math.abs(intersectionPoint % screen.height)

    } else if (intersectionPoint > screen.height) {

      intersectionPoint -= intersectionPoint % screen.height;

    }

   

    // 移动到预计落点

    if (intersectionPoint > player.y + (player.height >> 2)) {

      player.vy = PADDLE_SPEED;// 向下移动

    } else if (intersectionPoint < player.y - (player.height >> 2)) {

      player.vy = -PADDLE_SPEED; // 向上移动

    } else {

      player.vy = 0;// 已在正确位置

    }

}

6. 特殊功能系统
多球模式控制
javascript
// 添加球(消耗2分)

controller.A.onEvent(ControllerButtonEvent.Pressed, () => addBall(info.player1));

function addBall(player: info.PlayerInfo) {

    player.changeScoreBy(-2);

    createBall();

}



// 移除球(消耗2分,至少保留1个)

controller.B.onEvent(ControllerButtonEvent.Pressed, () => removeBall(info.player1));

function removeBall(player: info.PlayerInfo) {

    const balls = sprites.allOfKind(SpriteKind.Enemy);

    if (balls.length > 1) {

      Math.pickRandom(balls).destroy();

      player.changeScoreBy(-2);

    }

}

视觉反馈系统

javascript

game.onShade(function () {

    if (pingMessage) {

      screen.printCenter("ping", 5);

    } else {

      screen.printCenter("pong", 5);

    }

})

驴友花雕 发表于 2025-9-25 09:56:36

【花雕动手做】基于 Kitronik 游戏机开发板之乒乓球游戏

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



实验场景记录





页: [1]
查看完整版本: 【花雕动手做】基于 Kitronik 游戏机开发板之乒乓球游戏