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

【花雕动手做】Kitronik 可编程游戏开发板基于 ARCADE MakeCode之乒乓球游戏 简单

头像 驴友花雕 2025.09.25 6 0

00 (3).jpg

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

 

00 (4).jpg

作为学习、练习与尝试,这里创建一个乒乓球的小游戏。
打开网页版: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;
            })[0];
    }

    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;
        }
    }
});

这是一个功能丰富的双人乒乓球游戏,支持玩家对战和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;
   })[0];
}
物理轨迹预测算法
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);
   }
})

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

 

00218--.gif


实验场景记录

 

177 (1).jpg
177 (2).jpg

评论

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