Doodle Riddle - 一个 JavaScript Windows 8 游戏





5.00/5 (5投票s)
使用 JavaScript 和 HTML5 为 Windows 8 开发的猜谜游戏
引言
涂鸦谜题是 AppInnovation 竞赛中的一个 HTML5/JavaScript 项目。它是一款正在开发的 Windows 8 风格游戏。本文将概述该应用程序,并简要介绍使用 JavaScript 为 Windows 8 进行游戏开发的入门知识。它利用加速度计、倾斜计、触摸输入以及键盘来控制游戏。
游戏描述
游戏和术语的简要介绍。玩家(一只刺猬)必须通过移动到目标(一面旗帜)来逃离迷宫。一种模式通过按下箭头键、倾斜电脑或在屏幕上滑动到所需方向来开始。玩家将持续移动,直到到达边界或墙壁。如果没有边界,玩家将重新出现在游戏区域的另一侧。模式是有限的,所以要抓紧时间到达目标并继续下一关,或者如果你失败了,就重试本关。
游戏开发
无论你在哪里开发游戏,它总是由相同的阶段组成。它有一个初始化阶段、一个主循环,有时还有一个清理阶段。
在初始化状态下,所有资源将被加载并为以后使用做好准备,游戏区域将被准备好,并且所有元素(如玩家)将被设置。主循环由两个方法组成:update
和 draw
。这个循环会尽可能快地重复(至少像你期望的每秒帧数一样频繁)。Update
将计算游戏世界中的变化,而 draw
将把改变后的游戏世界显示给玩家。在更新或绘制期间来自用户的所有输入都需要保留到下一次更新,以便在下一次更新中观察到它们。
如何在 JavaScript 中实现主循环?
通常,你需要一个定时器,其定时为 1/60 秒,以实现每秒 60 帧。定时器超时后,它将回调并需要刷新。幸运的是,JavaScript 为我们提供了一个具有恒定定时功能的定时器,即 window.requestAnimationFrame
函数。因此,我们将从一个由 init
、update
、draw
和 **主循环** 组成的骨架开始,如下所示:
function init () { };
function update() { };
function draw () { }
function gameLoop() {
update();
draw();
window.requestAnimationFrame(gameLoop);
};
如何表示关卡?
表示关卡的最简单方法是将游戏区域序列化为某种 string
。让我们定义 P
代表玩家,W
代表墙壁,G
代表目标,B
代表边界。然后,你可以将你的关卡定义为一个 string
数组,并附带一些关于 maxMoves
和关卡尺寸的额外信息。
var levels = [{
field:
"BBBBBBBB BBBBBBBB"+
"B B"+
"B B"+
"B G B"+
"B B"+
"B B"+
" P W "+
"B B"+
"B B"+
"B B"+
"B B"+
"BBBBBBBB BBBBBBBB"
,
rows: 12,
cols: 17,
maxMoves: 2,
}, { ... } , { ... } ] ;
预加载一些资源
在加载关卡之前,我们需要讨论预加载资源。这个游戏主要使用一些精灵图。我们将在 default.html 页面中直接定义它们。为了确保图像不会显示出来,让我们将它们包裹在一个 <div style="display:none;" />
中。
<div id="imgPreload" style="display: none">
<img id="imgBorder1" src="https://codeproject.org.cn/images/tile01.png"/>
<img id="imgBorder2" src="https://codeproject.org.cn/images/tile02.png"/>
<img id="imgBorder3" src="https://codeproject.org.cn/images/tile03.png"/>
<img id="imgBorder4" src="https://codeproject.org.cn/images/tile04.png"/>
<img id="imgWall" src="https://codeproject.org.cn/images/tile05.png"/>
<img id="imgPlayer" src="https://codeproject.org.cn/images/hedgehog.png"/>
<img id="imgGoal" src="https://codeproject.org.cn/images/goal.png"/>
</div>
游戏将在一个 canvas
中进行。在那里,我们可以在指定的位置和大小进行绘制。这个 canvas
也需要放置在主页面 default.html 上。最好是放置在客户端区域的第一个子元素。<canvas id="gameCanvas">
你应该永远看不到这个 " />
</canvas>
。你可能想使用一些 CSS 来设置屏幕上的位置。
现在我们可以定义 init
函数,在该函数中我们将用页面中的值填充内部变量。此外,我们将定义两个回调函数,用于更新游戏状态,以及一个带有参数以成功完成关卡的函数。
// some golbal size definitions
var width = 850, height = 600;
var scaleX = 50, scaleY = 50;
// our canvas to draw on
var canvas;
var ctx;
// our preloaded resources
var spriteBorder;
var spritePlayer;
var spriteGoal;
var spriteWall;
// same callback functions
var updateStatus;
var levelFinished;
// keeps the current level
var currentLevel;
// the initialize function (loading the resources)
function init(statusCallback, levelCallback) {
canvas = document.getElementById('gameCanvas');
ctx = canvas.getContext('2d');
canvas.width = width;
canvas.height = height;
updateStatus = statusCallback;
levelFinished = levelCallback;
spriteBorder = document.getElementById("imgBorder1");
spritePlayer = document.getElementById("imgPlayer");
spriteGoal = document.getElementById("imgGoal");
spriteWall = document.getElementById("imgWall");
}
游戏世界与屏幕世界
游戏世界由 17x12 个格子组成。屏幕世界为 850x600 像素。游戏世界用一维数组表示。屏幕世界是二维的。因此,我们需要一些函数来从游戏世界转换为屏幕世界。
function pos2Point(pos) {
return {
x: (pos % currentLevel.cols) * scaleX,
y: ((pos - (pos % currentLevel.cols)) / currentLevel.cols) * scaleY };
}
加载关卡
现在我们准备加载关卡了。为此,我们需要一些额外的定义。默认速度(3 似乎不错),一个将方向转换为二维速度的对象,以及一个稍后告诉我们游戏是否已运行的变量。此外,我们需要一些变量来表示更多游戏元素。
var velocity = 3;
var epsilon = 1;
var dir = Object.freeze({ "up":{ x: 0, y: -velocity },"down": { x: 0, y: velocity },
"left":{ x: -velocity, y: 0 }, "right":{ x: velocity, y: 0 },
"none": { x: 0, y: 0 } });
// game elements
var borders;
var walls;
var goal;
var player;
var gameRunning;
var movesLeft;
// loads and starts a level
function loadLevel(nr) {
if (window.Puzzle.levels.length <= nr) return false;
currentLevel = window.Puzzle.levels[nr];
currentLevelNr = nr;
movesLeft = currentLevel.maxMoves;
borders = [];
walls = [];
goal = null;
player = null;
ctx.clearRect(0, 0, width, height);
for (var i = 0; i < currentLevel.field.length; i++) {
switch (currentLevel.field[i]) {
case "P":
player = { position: pos2Point(i), sprite: spritePlayer, direction: dir.none };
break;
case "B":
var b = { position: pos2Point(i), sprite: spriteBorder };
borders.push(b);
break;
case "W":
var w = { position: pos2Point(i), sprite: spriteWall };
walls.push(w);
break;
case "G":
goal = { position: pos2Point(i), sprite: spriteGoal };
break;
}
}
updateStatus(currentLevelNr, movesLeft);
gameRunning = true;
gameLoop();
return true;
}
移动玩家
为了移动玩家,我们定义了一个接受方向对象的 move
函数。在玩家移动时,我们将不接受任何方向更改。如果玩家执行了新的操作,我们需要更新我们的状态。这将通过调用 updateStatus
回调函数来完成。
function move(dir) {
if (player.direction == dir.none) {
movesLeft -= 1;
updateStatus(currentLevelNr, movesLeft);
player.direction = dir;
}
Update 函数
如果玩家在墙壁或边界处停止,我们需要确保我们在网格内,并且还有足够的移动次数可以继续。如果没有,我们需要成功或不成功地结束游戏。
function stopPlayer() {
// stop the player and ensure we are snapped to the playfield grid
player.direction = dir.none;
player.position.x = Math.round(player.position.x / scaleX) * scaleX;
player.position.y = Math.round(player.position.y / scaleY) * scaleY;
}
function endGame(success) {
// stop the game and return the result
gameRunning = false;
ctx.clearRect(0, 0, width, height);
levelFinished(success);
}
update
函数本身是游戏的核心 " />。在这里,我们将检查玩家的位置以及该做什么。如果我们离开场地,只需跳到对面边界。如果我们靠近墙壁或边界,则停止当前移动;如果我们在目标处,则成功退出。最后但同样重要的是,如果没有剩余移动次数,则不成功退出。
function update() {
// calculate new player pos
var playerNewPos = { x: player.position.x += player.direction.x,
y: player.position.y += player.direction.y };
// check the border
if (playerNewPos.x < 0) playerNewPos.x = width - scaleX;
if (playerNewPos.y < 0) playerNewPos.y = height - scaleY;
if (playerNewPos.x > width - scaleX) playerNewPos.x = 0;
if (playerNewPos.y > height - scaleY) playerNewPos.y = 0;
//check borders & walls
borders.forEach(function(b) {
if ((Math.abs(playerNewPos.x - b.position.x) < scaleX &&
Math.abs(playerNewPos.y - b.position.y) <= epsilon) ||
(Math.abs(playerNewPos.y - b.position.y) < scaleY &&
Math.abs(playerNewPos.x - b.position.x) <= epsilon)) {
stopPlayer();
if (movesLeft <= 0) { endGame(false); }
}
});
walls.forEach(function(w) {
if ((Math.abs(playerNewPos.x - w.position.x) < scaleX &&
Math.abs(playerNewPos.y - w.position.y) <= epsilon) ||
(Math.abs(playerNewPos.y - w.position.y) < scaleY &&
Math.abs(playerNewPos.x - w.position.x) <= epsilon)) {
stopPlayer();
if (movesLeft <= 0) { endGame(false); }
}
});
// check for goal
if ((Math.abs(playerNewPos.x - goal.position.x) < epsilon &&
Math.abs(playerNewPos.y - goal.position.y) <= epsilon) ||
(Math.abs(playerNewPos.y - goal.position.y) < epsilon &&
Math.abs(playerNewPos.x - goal.position.x) <= epsilon)) {
stopPlayer();
endGame(true);
}
// accept the move ?
if (player.direction != dirEnum.none) {
player.position = playerNewPos;
}
}
Draw 函数
draw
函数将清除 canvas
并将所有游戏对象绘制到它们的位置。
function draw() {
// draw the playfield
ctx.clearRect(0, 0, width, height);
borders.forEach(function (b) {
ctx.drawImage(b.sprite, b.position.x, b.position.y, scaleX, scaleY);
});
walls.forEach(function (w) {
ctx.drawImage(w.sprite, w.position.x, w.position.y, scaleX, scaleY);
});
ctx.drawImage(goal.sprite, goal.position.x, goal.position.y, scaleX, scaleY);
ctx.drawImage(player.sprite, player.position.x, player.position.y, scaleX, scaleY);
}
最终的游戏循环看起来像这样,这样我们就可以在关卡完成后停止它。
function gameLoop() {
update();
draw();
if (gameRunning) window.requestAnimationFrame(gameLoop);
};
整合
为了能够重新开始并继续下一关,我们将需要两个辅助函数。
function runNextLevel() {
return loadLevel(currentLevelNr + 1);
}
function retryLevel() {
loadLevel(currentLevelNr);
return currentLevelNr;
}
将所有变量和函数放入一个模块中,以保持主命名空间的整洁 " />。
(function(){
// variables and functions of the game as described above
window.Puzzle.direction = dir;
window.Puzzle.init = init;
window.Puzzle.hideMenu = hideMenu;
window.Puzzle.loadLevel = loadLevel;
window.Puzzle.move = move;
window.Puzzle.runNextLevel = runNextLevel;
window.Puzzle.retryLevel = retryLevel;
})()
菜单
游戏应该有一些菜单来显示游戏状态。让我们在主页面 default.html 上定义一些额外的 div
来显示菜单内容。
<div id="mainMenu">
<h1>doodle riddle</h1>
<h3>for windows 8</h3>
<h3>...by <a href="http://twitter.com/dotnetbird/" target="_blank">Matthias Fischer</a></h3>
<a class="button" id="btnStart">Start new game</a>
<div id="info" class="info">
<!- Instruction how to play here -->
</div>
</div>
<div id="gameOverMenu">
<div class="bigMsg">The game is over !</div>
<a class="button" id="btnGameOver">Main menu</a>
</div>
<div id="nextLevelMenu">
<div class="bigMsg">Level Solved</div>
<a class="button" id="btnContinue">Continue</a>
<a class="button" id="btnMainMenu">Main menu</a>
</div>
根据游戏状态,我们将显示 main
、nextlevel
或 gameOver
菜单。
在启动应用程序后,我们需要初始化我们的游戏。这将通过 initialize
函数完成。根据你的框架,你需要不同的代码来启用按钮和显示 div
(你准备好的菜单)。
window.Puzzle.init(
function (level,moves) {
// update your divs with the level and with the moves left info
},
function (success) {
if (success) {
// show the nextLevelMenu
// and enable the continue button
} else {
// show the game over menu and enable the mainMenuButton
}
});
添加一些键盘监听器来告知游戏下一步的移动。
document.body.addEventListener('keyup', function (e) {
if (e.keyCode === WinJS.Utilities.Key.leftArrow) {
window.Puzzle.move(window.Puzzle.direction.left);
} else if (e.keyCode === WinJS.Utilities.Key.rightArrow) {
window.Puzzle.move(window.Puzzle.direction.right);
} else if (e.keyCode === WinJS.Utilities.Key.upArrow) {
window.Puzzle.move(window.Puzzle.direction.up);
} else if (e.keyCode === WinJS.Utilities.Key.downArrow) {
window.Puzzle.move(window.Puzzle.direction.down);
}
}, false);
如何检测电脑是否倾斜?
为此,我们将使用倾斜计传感器。该传感器将告诉我们角度是否有变化。对于我们的游戏,我们需要 **俯仰** 和 **翻滚** 值。如果俯仰角增加,则向上移动;如果减小,则向下移动。翻滚值也是如此,如果增加,则向左移动,否则向右移动。如果自上次读取以来没有变化,则不执行任何操作。请注意,我们需要大约 5 度的阈值(你可能需要进行一些微调)。定时应约为 500 毫秒。
var lastPitch;
var lastRoll;
var inclinometer = Windows.Devices.Sensors.Inclinometer.getDefault();
if (inclinometer) {
inclinometer.reportInterval = 500; //wait 0.5sek
inclinometer.addEventListener("readingchanged", function (e) {
var reading = e.reading;
var pitch = reading.pitchDegrees.toFixed(2);
var roll = reading.rollDegrees.toFixed(2);
var pitchDist = lastPitch - pitch;
var rollDist = lastRoll - roll;
if (Math.abs(pitchDist) > 5) {
window.Puzzle.move((pitchDist > 0)
? window.Puzzle.direction.up : window.Puzzle.direction.down);
} else if (Math.abs(rollDist) > 5) {
window.Puzzle.move((rollDist > 0)
? window.Puzzle.direction.left : window.Puzzle.direction.right);
}
lastPitch = pitch;
lastRoll = roll;
});
}
许可证
本文以及任何相关的源代码和文件,均根据 The Code Project Open License (CPOL) 许可。你可以自由地在自己的项目中使用的代码,只要你通过电子邮件告诉我你正在这样做,并且只要你不发布我游戏的克隆版本 " />。
历史
- 2012.10.22 - 初版