【花雕动手做】基于 Kitronik 游戏机开发板之乒乓球游戏
Kitronik ARCADE 使用 Microsoft MakeCode 平台,具有以下优势:
图形化编程界面:适合初学者,支持拖拽式编程。
即时模拟器:可以实时测试游戏效果。
硬件兼容性:可部署到 Kitronik ARCADE 设备,实现实体游戏体验。
支持 Python/JavaScript:便于进阶学习。
【花雕动手做】基于 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;
}
}
});
【花雕动手做】基于 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);
}
})
【花雕动手做】基于 Kitronik 游戏机开发板之乒乓球游戏
通过模拟器,调试与模拟运行实验场景记录
页:
[1]