65.9K
CodeProject 正在变化。 阅读更多。
Home

Doodle Riddle - 一个 JavaScript Windows 8 游戏

starIconstarIconstarIconstarIconstarIcon

5.00/5 (5投票s)

2012年10月22日

CPOL

6分钟阅读

viewsIcon

17518

使用 JavaScript 和 HTML5 为 Windows 8 开发的猜谜游戏

引言

涂鸦谜题是 AppInnovation 竞赛中的一个 HTML5/JavaScript 项目。它是一款正在开发的 Windows 8 风格游戏。本文将概述该应用程序,并简要介绍使用 JavaScript 为 Windows 8 进行游戏开发的入门知识。它利用加速度计、倾斜计、触摸输入以及键盘来控制游戏。

doodle riddle

游戏描述

游戏和术语的简要介绍。玩家(一只刺猬)必须通过移动到目标(一面旗帜)来逃离迷宫。一种模式通过按下箭头键、倾斜电脑或在屏幕上滑动到所需方向来开始。玩家将持续移动,直到到达边界或墙壁。如果没有边界,玩家将重新出现在游戏区域的另一侧。模式是有限的,所以要抓紧时间到达目标并继续下一关,或者如果你失败了,就重试本关。

游戏开发

无论你在哪里开发游戏,它总是由相同的阶段组成。它有一个初始化阶段、一个主循环,有时还有一个清理阶段。

doodle riddle

在初始化状态下,所有资源将被加载并为以后使用做好准备,游戏区域将被准备好,并且所有元素(如玩家)将被设置。主循环由两个方法组成:updatedraw。这个循环会尽可能快地重复(至少像你期望的每秒帧数一样频繁)。Update 将计算游戏世界中的变化,而 draw 将把改变后的游戏世界显示给玩家。在更新或绘制期间来自用户的所有输入都需要保留到下一次更新,以便在下一次更新中观察到它们。

如何在 JavaScript 中实现主循环?

通常,你需要一个定时器,其定时为 1/60 秒,以实现每秒 60 帧。定时器超时后,它将回调并需要刷新。幸运的是,JavaScript 为我们提供了一个具有恒定定时功能的定时器,即 window.requestAnimationFrame 函数。因此,我们将从一个由 initupdatedraw 和 **主循环** 组成的骨架开始,如下所示:

  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"> 你应该永远看不到这个 眨眼 | <img src= " /> </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 函数本身是游戏的核心 眨眼 | <img src= " />。在这里,我们将检查玩家的位置以及该做什么。如果我们离开场地,只需跳到对面边界。如果我们靠近墙壁或边界,则停止当前移动;如果我们在目标处,则成功退出。最后但同样重要的是,如果没有剩余移动次数,则不成功退出。

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

将所有变量和函数放入一个模块中,以保持主命名空间的整洁 眨眼 | <img src= " />。

(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>

根据游戏状态,我们将显示 mainnextlevelgameOver 菜单。

在启动应用程序后,我们需要初始化我们的游戏。这将通过 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 毫秒。

doodle riddle

     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) 许可。你可以自由地在自己的项目中使用的代码,只要你通过电子邮件告诉我你正在这样做,并且只要你不发布我游戏的克隆版本 眨眼 | <img src= " />。

历史

  • 2012.10.22 - 初版
© . All rights reserved.