JavaScript游戏开发实例指南

译文
开发 前端 游戏开发
。任何一种知名编程语言都可用于游戏开发(当然,某些语言会让游戏开发流程变得更加困难)。总而言之,游戏开发虽非易事,但也并不一定门槛过高。在今天的文章中,我们就一起探讨如何以简便的方式打造属于自己的游戏作品。

简介

对我来说,开发游戏不仅仅只是种爱好、更源于某种偏执。游戏开发往往是项复杂的工作,涉及的功能以及吸引玩家关注的因素越多、开发过程就越艰难。任何一种知名编程语言都可用于游戏开发(当然,某些语言会让游戏开发流程变得更加困难)。总而言之,游戏开发虽非易事,但也并不一定门槛过高。在今天的文章中,我们就一起探讨如何以简便的方式打造属于自己的游戏作品。

背景信息

虽然没有明确证据,但JavaScript很可能已经成为全球使用率最高的通用语言。能取得这样的成绩,也要归功于互联网的迅猛发展以及HTML 5在平板及移动设备上得到的广泛支持。

有鉴于此,我决定在本文中向大家介绍如何利用HTML5与JavaScript进行游戏开发。

在开始之前,我们先来介绍关于游戏开发的背景知识:

Game Loop

游戏开发工作中最重要的元素被称为Game Loop,意为“游戏循环”,主要负责游戏的流程体系绘制工作。游戏循环的存在保证了游戏能够继续进行而不会被用户的操作所打断。

那么该如何建立游戏循环?在下面的示例中,我将按照大多数开发人员的直观感受首先创建后台进程,不过这其实并非最有效的解决方案。

while (!MyGame.stopped) {  
  MyGame.update();  
}  
  • 1.
  • 2.
  • 3.

游戏循环的主要作用之一在于调用update函数,即负责游戏内容的更新(包括图形及对象位置等)。

如果我们使用While语句来调用update函数,那么一方面浏览器很可能阻断该函数,另一方面我们也无法控制update函数的调用频率、导致执行效率低下。换言之,我们将无法控制游戏的FPS比率。

另一种常用的游戏循环创建方式则是利用setInterval函数,通过这种方式我们能够更轻松地控制update函数的调用频率:

MyGame.fps = 60; 
MyGame.update = function () { 
    // Move game parts 
}; 
// Start the game loop 
MyGame._intervalId = setInterval(Game.update, 1000 / Game.fps); 
// To stop the game, use the following: 
clearInterval(MyGame._intervalId);  
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.

 

以上代码中出现的“FPS”指的可不是第一人称射击,而是每秒帧数,也就是我们常说的帧速率(也被称为帧频率)。它负责控制设备每秒钟显示不同画面内容的次数。人眼与大脑每秒钟最多只能分辨10到12幅独立画面,不过为了达到流畅的画面效果(例如游戏与电影),FPS值需要设置得更高一些。

第一部分-从EASEJS入手

尽管Game Loop的创建看似简单,但以此为基础进行游戏开发实在麻烦多多。下面我将利用EASEJS库所提供的Game Loop及其它相关功能打造自己的游戏作品。

EaselJS提供了一套非常直接的解决方案,能够容纳丰富的图形元素并与HTML 5 Canvas进行交互。它还提供一套专用API,旨在为Flash开发者提供更为熟悉的使用体验且同时能够识别JavaScript。它包含一套完整的分层显示列表、一套核心交互模型以及帮助类等等,从而显著简化了Canvas的使用流程。

  • Canvas元素是HTML 5中的组成部分,支持动态、脚本化2D图形与位图图像渲染功能。这是一套低级程序化模型,只能更新位图图形、不具备内置场景图像。
  • EASEJS下载地址 - http://www.createjs.com/#!/EaselJS

 

创建新的HTML页面并在代码头中添加以下内容。

<script type="text/javascript" src="js/easeljs/geom/Matrix2D.js"></script> 
<script type="text/javascript" src="js/easeljs/geom/Rectangle.js"></script> 
<script type="text/javascript" src="js/easeljs/events/EventDispatcher.js"></script> 
<script type="text/javascript" src="js/easeljs/utils/UID.js"></script> 
<script type="text/javascript" src="js/easeljs/utils/Ticker.js"></script> 
<script type="text/javascript" src="js/easeljs/utils/SpriteSheetUtils.js"></script> 
<script type="text/javascript" src="js/easeljs/display/DisplayObject.js"></script> 
<script type="text/javascript" src="js/easeljs/display/Container.js"></script> 
<script type="text/javascript" src="js/easeljs/display/Stage.js"></script> 
<script type="text/javascript" src="js/easeljs/display/SpriteSheet.js"></script> 
<script type="text/javascript" src="js/easeljs/display/BitmapAnimation.js"></script> 
<script type="text/javascript" src="js/easeljs/display/Graphics.js"></script> 
<script type="text/javascript" src="js/easeljs/display/Bitmap.js"></script> 
<script type="text/javascript" src="js/easeljs/display/Text.js"></script> <span style="font-size: 9pt;"> </span>  
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.

将Canvas元素添加到Body部分,这样Canvas就会成为游戏的容器环境。

<canvas id="gameCanvas" width="600" height="100"></canvas>  
  • 1.

 现在我们开始处理EASEJS,首先要做的是创建一个新的对象类型——Stage。

所谓stage,是指专为显示列表设置的基础Container。每当我们调用其Stage/tick方法,它都会将显示列表呈现在目标canvas当中。

stage = new createjs.Stage(id("gameCanvas"));  
  • 1.

 在完成了Stage对象的初始化之后,我们可以开始游戏中添加更多元素。接下来是创建Bitmap对象(也是EASEJS中的组成部分)用于接收图形路径,然后利用addChild函数向Stage中添加新的Bitmap对象。当所有游戏元素都被添加到Stage对象当中后,我们需要通过调用createjs.Ticker.setFPS(30)来启动游戏循环;最后登记“tick”事件(这样每当tick事件被触发时,游戏都会进行一次更新)。

Ticker会根据设定好的时间间隔提供集中式tick或心跳广播。监听器可订阅该tick事件以确保每隔固定时段即获得通知。

需要注意的是,tick事件的时间间隔被称为“目标间隔”,一旦CPU利用率较高、其间隔时间就会受到影响。Ticker类使用静态接口(例如Ticker.getPaused()),且不应被实例化。

var stage, image, fpstext; 
function init() { 
    stage = new createjs.Stage(id("gameCanvas")); 
    image = new createjs.Bitmap("assets/hill.png"); 
    stage.addChild(image); 
    createjs.Ticker.setFPS(30); 
    createjs.Ticker.addEventListener("tick", tick); 
}  
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.

 现在我们需要执行tick函数。在本文的示例中,我们将把图形向右侧移动10个像素,一旦图形达到600像素位移距离,我们则将其返回到初始位置(x=0)。在改变了图形的属性之后,我们需要调用stage.update()函数来刷新canvas

function tick() { 
    image.x += 10; 
    if (image.x > 600) 
        image.x = 0; 
    stage.update(); 
}  
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.

在观察结果之前,我们再向stage中添加一个新元素,即text元素。我希望在Stage中直接显示FPS数值。要在stage中增加文本信息,我们要向Init函数中添加以下

fpstext.x = 0; 
fpstext.y = 20; 
stage.addChild(fpstext);  
  • 1.
  • 2.
  • 3.

 

而要添加当前当前FPS速率,则需要向以下代表添加至tick函数当中:

fpstext.text = Math.round(createjs.Ticker.getMeasuredFPS()) + " fps";  
  • 1.

  • 1.

点击此处查看演示效果。

#p#

第二部分-Sprite

在第一部分中,我们了解了关于EASEJS的基本知识,包括向Stage中添加元素以及如何移动这些元素。在第二部分中,我们将一同利用Sprite以多种方式显示我们的角色对象(例如跳、跑以及其它小动作)。

Sprite专指没有经过照明、效果等转换通道处理而是直接绘制得到的2D位图显示目标。Sprite通常被用于显示游戏内的简单信息,例如体力条、生命数量或者当前分数等文本内容。

一幅Sprite图形中包含若干子图像,我们可以通过指定图形坐标的方式显示其中的特定子图像。

首先,我们先将Player Sprite载入到游戏当中。需要强调的是,请务必确定图像已经载入完成后再启动游戏循环。在范例中,我登记了onload事件,只有载入事件结束后、我才能执行start函数。


 

首先,我们先将Player Sprite载入到游戏当中。需要强调的是,请务必确定图像已经载入完成后再启动游戏循环。在范例中,我登记了onload事件,只有载入事件结束后、我才能执行start函数。

var stage, player, playerImage, _action; 
var playerWH = 64; 
var frequency = 4; 
function init() { 
    playerImage = new Image(); 
    playerImage.src = "assets/Player.png"
    playerImage.onload = start; 
}  
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.

在第一部分中,我们利用Bitmap对象在stage中显示出图像。但既然现在开始使用Sprite,我们就需要引入有能力处理Sprite图形的不同对象才行。

SpriteSheet - 旨在将所有与sprite列表相关的属性与方法都打包保存。一套sprite列表中包含一系列图像(通常是单独的动画帧)结合而成的大型图像。举例来说,如果一小段动画中包含有八幅100x100的图片,那么整个sprite列表的尺寸则为400x200(每行四幅图片、共两行)。

在创建了Stage对象之后,我们还要创建playerSprite作为SpriteSheet对象的代表。所有传递至SpriteSheet构造函数的数据都用于定义三类关键信息:

  1. 单张或多张图片的使用方式。
  2.  单独图形帧的显示位置。此数据可通过两种方式表示:作为连续规则网格中的等尺寸帧一同处理,或者作为不规则(非顺序)网格中可变尺寸帧被单独定义。
  3. 同样,动画也可通过两种方式表现:作为一系列连续动画帧(拥有开始与结束帧定义)中的一部分,例如[0,3];或者直接以动画帧列表的形式表现,例如[0,1,2,3]。
function start() { 
    stage = new createjs.Stage(id("gameCanvas")); 
    var playerSprite = new createjs.SpriteSheet({ 
        animations: 
        { 
            "walk": [0, 9, null], 
            "fall": [10, 21, null], 
            "jump": [22, 32, null], 
            "gamgam": [34, 64, null], 
            "stand": [44, 44, null], 
            "special_combo": [22, 32, "gamgam"
        }, 
        images: [playerImage], 
        frames: 
            { 
                height: playerWH, 
                width: playerWH, 
                regX: 0, 
                regY: 0, 
            }  
<span style="font-size: 9pt;">    });</span>  
<span style="font-size: 9pt;">}  </span><span style="font-size: 9pt;"> </span> 
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.

上述代码通过指定图形开始与结束ID的方式定义了角色的跳跃、行走、跌倒等动作。在下面的图片中,我们可以看到行走动作为反复显示从id0id9各图片。

在加载了sprite并定义了所有角色动作之后,我们要使用BitmapAnimation对象帮助自己将不同动作串连起来,从而实现动画效果。

BitmapAnimation - 显示sprite列表中的多幅图片或者连续图片(例如动画)。一套sprite列表中包含有一系列图片(通常属于动画帧),且各子图片被汇总成一份整体大图。举例来说,如果一段动画中包含8幅100x100尺寸的独立图片,那么sprite列表中的整体图片尺寸为400x200(每行四幅图片、共两行)。大家可以单独显示其中的一幅图片、以动画形式显示多幅画片甚至将不同动画串连在一起。

在start函数中添加以下代码以显示行走动作。

player = new createjs.BitmapAnimation(playerSprite);  
player.x = id("gameCanvas").width / 2;  
player.y = id("gameCanvas").height - playerWH;  
//player.currentFrame = 2;  
player.gotoAndPlay("walk");  
stage.addChild(player);  
createjs.Ticker.setFPS(60);  
createjs.Ticker.addEventListener("tick", tick);   
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.

现在大家可以在屏幕中观察角色的行走及其它动作。我还添加了以下代码帮助自己随时掌握游戏运行时的FPS、速度及频率变化。

switch (_id) { 
    case "fps"
        createjs.Ticker.setFPS(parseInt(value)); 
        break
    case "velocity"
        player.vX = parseInt(value); 
        break
    case "frequency"
        frequency = parseInt(value); 
        start(); 
        break
}   
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.

点击此处查看演示效果。

#p#

第三部分 – 移动角色

在第二部分中,我们演示了如何利用sprite以不同方式显示我们的角色(例如跳跃、行走、跌倒等)。现在我们将一起学习如何让角色在游戏空间中移动。

为了实现移动功能,我们需要对tick函数做以如下修改(不影游戏循环的update):

if (player.x >= id("gameCanvas").width - playerWH) { 
    // You reached the end -  We need to walk left 

if (player.x < playerWH) { 
    // You reached the end -  We need to walk right 
}  
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.

在以上代码中,我们定义了游戏的边界区域。如果角色到达游戏空间的右侧边界,就会被直接转移至左侧边界,反之亦然。

要改变角色的面对方向(左或右),我们要将direction属性作为角色对象(BitmapAnimation)中的一部分。

if (player.x >= id("gameCanvas").width - playerWH) { 
    // You reached the end -  We need to walk left 
    player.direction = -90; 

if (player.x < playerWH) { 
    // You reached the end -  We need to walk right 
    player.direction = 90; 
}   
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.

改变角色的朝向后,我们需要通过变更角色的x属性实现位置移动。

player.direction == 90 ? player.x += player.vX : player.x -= player.vX;  
  • 1.

 经过修改,完整的tick函数代码如下所示:

function tick() {            
   if (player.x >= id("gameCanvas").width - playerWH) { 
       // You reached the end -  We need to walk left 
       player.direction = -90; 
       player.gotoAndPlay("walk"
   } 
 
   if (player.x < playerWH) { 
       // You reached the end -  We need to walk right 
       player.direction = 90; 
       player.gotoAndPlay("walk_h"); 
   } 
 
   // Moving the sprite based on the direction & the speed 
   player.direction == 90 ? player.x += player.vX : player.x -= player.vX; 
     
stage.update(); 
}  
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.

现在我们遇上了新难题——当角色向右移动时,其面部朝向仍然向左而没有正确翻转。

 

我们可以利用CCS 3 scaleX实现图像翻转,但这会大幅提升游戏的性能需求。EASEJS为我们提供了解决方案,大家可以利用addFlippedFrames函数,该函数能对现有sprite列表进行扩展,从而实现原始帧图像的横向及纵向翻转,并加入适当的过渡动画帧数据。翻转后的动画会在名称后加入后缀(例如_h_v等)。要使用此方法前,请务必确保游戏已经将全部sprite列表图片载入完毕。

在创建角色对象之前,我们还需要加入以下代码:

//( spriteSheet  horizontal  vertical  both )  
createjs.SpriteSheetUtils.addFlippedFrames(playerSprite, truefalsefalse);  
  • 1.
  • 2.

start函数的内容将如下所示:

function start() { 
stage = new createjs.Stage(id("gameCanvas")); 
 
var playerSprite = new createjs.SpriteSheet({ 
    animations: 
    { 
        "walk": [0, 9, null, frequency], 
        "fall": [10, 21, null, frequency], 
        "jump": [22, 32, null, frequency], 
        "gamgam": [34, 64, null, frequency], 
        "stand": [44, 44, null, frequency], 
        "special_combo": [22, 32, "gamgam"
    }, 
    images: [playerImage], 
    frames: 
        { 
            height: playerWH, 
            width: playerWH, 
            regX: 0, 
            regY: 0, 
        } 
}); 
 
createjs.SpriteSheetUtils.addFlippedFrames(playerSprite, truefalsefalse); 
 
var fps = parseInt(id("fps").value); 
var velocity = parseInt(id("velocity").value); 
 
player = new createjs.BitmapAnimation(playerSprite); 
player.x = id("gameCanvas").width / 2; 
player.y = id("gameCanvas").height - playerWH; 
player.vX = velocity; 
_action = "walk"
player.gotoAndPlay("walk"); 
 
stage.addChild(player); 
 
createjs.Ticker.setFPS(fps); 
createjs.Ticker.useRAF = true
createjs.Ticker.addEventListener("tick", tick); 

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.

 在使用addFlippedFrame函数之后,每段动画(例如跳跃、行走等)都将通过复制实现另一种朝向的显示效果,翻转后的图片则以_h作为名称结尾。这样如果大家希望利用翻转图片表现向左行走的效果,则可调用gotoAndPlay函数并添加“walk_h”动作名称。

function tick() { 
    if (_action.indexOf("walk") != -1) { 
        if (player.x >= id("gameCanvas").width - playerWH) { 
            // You reached the end -  We need to walk left 
            player.direction = -90; 
            player.gotoAndPlay("walk"
        } 
        if (player.x < playerWH) { 
            // You reached the end -  We need to walk right 
            player.direction = 90; 
            player.gotoAndPlay("walk_h"); 
        } 
        // Moving the sprite based on the direction & the speed 
        player.direction == 90 ? player.x += player.vX : player.x -= player.vX; 
    } 
    stage.update(); 
}   
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.

点击此处查看演示效果。

#p#

第四部分 - 创建游戏环境

在本章节中,我们要讨论的是游戏的“环境氛围”。当然,我们需要向游戏中加入背景及其它元素,包括静态图像和动画图像,从而为玩家带来舒适的环境体验。

在着手设计游戏之前,我们先花点时间聊聊PreloadJS库(与EaseJS一样属于CreateJS的一部分)。

PreloadJS提供一套一致性方式,旨在预先载入HTMl应用中要用到的内容。预载入可通过HTML标签以及XHR实现。

现在我们向html页面中加入新的JavaScript文件:

<script type="text/javascript" src="js/preloadjs-0.3.0.min.js"></script>  
  • 1.
  • 1.

然后添加另一个名为start的函数,此函数将负责在游戏正式开始之前载入所有游戏资源

function start() {            
    manifest = [ 
        { src: "assets/sky.png", id: "sky" }, 
        { src: "assets/ground.png", id: "ground" }, 
        { src: "assets/logo.png", id: "sun" }, 
        { src: "assets/hill.png", id: "hill" } 
    ]; 
    loader = new createjs.LoadQueue(false); 
    loader.onFileLoad = handleFileLoad; 
    loader.onComplete = handleComplete; 
    loader.loadManifest(manifest); 
}   
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.

如大家所见,我们定义了一个清单对象(可使用任何名称),并在其中罗列了游戏运行前必须载入的全部资源。

 一旦下载工作完成,onComplete事件将被调用,接着onFileLoad事件将调用每一项资源。

 下列代码用于向assets数组中添加所有资源:

var assets = []; 
function handleFileLoad(event) { 
    assets.push(event.item); 
}   
  • 1.
  • 2.
  • 3.
  • 4.

在我们进一步向游戏中添加图形之前,先来看下面这部分内容:

如下图所示,我们已经设计好一小块地面图形,大家可以轻松通过多次复制的方式使其成为一大片连续的地面。

Name

地面

山脉

太阳

天空

 

这里我们需要添加新的函数——handleComplete,负责添加所有来自assets数组的资源。需要强调的是,PreloadJS并不只是载入背景资源,而且还会根据其具体类型将资源进行登记并为其赋予惟一ID。为了从PreloadJS库中找回各个对象,我们需要以惟一ID为基础利用getResult函数实现该目标。

我们首先从布置地面图像入手。地面应该被多次复制直到铺满整个游戏环境的横向空间,这样角色才能始终在地面之上行动。第一步需要利用地面图像创建新的Shape对象。通过以下代码,大家可以看到Shape对象的w属性如何被设置为游戏环境的宽度。这种处理方法不会将图像拉长,而是以复制的方式平铺开来。

最后,我们将地面Shape对象添加到stage当中。

点击此处查看演示效果。

function handleComplete() { 
    buildPlayerSprite(); 
    for (var i = 0; i < assets.length; i++) { 
        var item = assets[i]; 
        var _id = item.id; 
        var result = loader.getResult(_id); 
        if (item.type == createjs.LoadQueue.IMAGE) { 
            var bmp = new createjs.Bitmap(result); 
        } 
        switch (_id) {           
            case "ground"
                ground = new createjs.Shape(); 
                var g = ground.graphics; 
                g.beginBitmapFill(result); 
                g.drawRect(0, 0, w, 79); 
                ground.y = h - 79; 
                break;           
        } 
    } 
    stage.addChild(ground, player); 
    player.gotoAndPlay("walk_h"); 
     
    var fieldValue = id("fps"); 
    var fps = parseInt(fieldValue.value); 
    createjs.Ticker.setFPS(fps); 
    createjs.Ticker.useRAF = true
    createjs.Ticker.addEventListener("tick", tick); 
}   
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.

 

在演示中,大家会看到角色开始在地面上持续奔跑。不过我们还希望添加一些动态效果,让地面看起来像是在与角色一同移动。

为了实现这一效果,我们需要更新tick函数并变更地面的x属性。

function tick() { 
          var outside = w + 20; 
        var position = player.x + player.vX; 
        player.x = (position >= outside) ? -200 : position; 
        ground.x = (ground.x - 10); 
    } 
    stage.update(); 
}  
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.

点击此处查看演示效果。

现在我们会看到地面已经在随着角色的行走而一同移动,不过由于宽度不够、地面的右侧被截断了,因此我们需要增加Shape宽度。

case "ground"
   ground = new createjs.Shape(); 
   var g = ground.graphics; 
   g.beginBitmapFill(result); 
   g.drawRect(0, 0, w+330, 79); 
   ground.y = h - 79; 
   break;  
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.

 

以上代码表示地面Shape的当前宽度为w+330,不过这还只是解决方案的一部分,因为地面Shape已经从屏幕中消失了。为了彻底解决问题,我们还需要通过添加额外的宽度模块来更新tick函数。

 通过以下代码,我们就能确保地面始终处于游戏的边界之内。

ground.x = (ground.x - 10) % 330;  
  • 1.

点击此处查看演示效果。

现在我们已经理解了如何向游戏stage中添加对象、创建效果与形状。下面我们要对游戏中的其它对象进行同样的调整。

*注意-我还加入了以下事件“stagemousedown”,旨在让游戏感知用户在stage中的点击操作。

unction handleComplete() { 
    buildPlayerSprite(); 
    for (var i = 0; i < assets.length; i++) { 
        var item = assets[i]; 
        var _id = item.id; 
        var result = loader.getResult(_id); 
        if (item.type == createjs.LoadQueue.IMAGE) { 
            var bmp = new createjs.Bitmap(result); 
        } 
        switch (_id) { 
            case "sky"
                var g = new createjs.Graphics() 
                g.beginBitmapFill(result); 
                g.drawRect(0, 0, w * 2, h) 
                sky = new createjs.Shape(g); 
                break
            case "ground"
                ground = new createjs.Shape(); 
                var g = ground.graphics; 
                g.beginBitmapFill(result); 
                g.drawRect(0, 0, w + 330, 79); 
                ground.y = h - 79; 
                break
            case "hill"
                hill = new createjs.Shape(new createjs.Graphics().beginBitmapFill(result).drawRect(0, 0, w, 159)); 
                hill.x = 0; 
                hill.scaleX = 3; 
                hill.y = 163; 
                break
            case "sun"
                var g = new createjs.Graphics(); 
                g.beginBitmapFill(result); 
                g.drawRect(0, 0, 129, 129); 
                sun = new createjs.Shape(g); 
                sun.x = w; 
                sun.y = 37; 
                break
        } 
    } 
    stage.addChild(sky, ground, hill, sun, player); 
    player.gotoAndPlay("walk_h"); 
    stage.addEventListener("stagemousedown"function () { 
        play("jump_h"); 
    }); 
    var fieldValue = id("fps"); 
    var fps = parseInt(fieldValue.value); 
    createjs.Ticker.setFPS(fps); 
    createjs.Ticker.useRAF = true
    createjs.Ticker.addEventListener("tick", tick); 
}  
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.
  • 49.
  • 50.

最后,在完成了对地面Shape的调整后,我们还需要改变tick函数来实现对象操作。这一次我添加了另一项参数,旨在计算游戏边界之外的对象。当一个对象消失在游戏边界处后,我们可以通过改变其x属性让它再次出现在游戏之中。

function tick() { 
    if (_action.indexOf("walk") != -1 || _action.indexOf("jump") != -1) { 
        var outside = w + 20; 
        var position = player.x + player.vX; 
        player.x = (position >= outside) ? -200 : position; 
        sky.x = (sky.x - 5) % w; 
        hill.x = (hill.x - 2) % w; 
        ground.x = (ground.x - 10) % 330; 
        sun.x = (sun.x - 1); 
        if (sun.x <= -135) { sun.x = outside + 50; } 
    } 
    stage.update(); 
}  
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.

点击此处查看演示效果。

 #p#

第五部分 - 碰撞与跳跃

现在我们来看本文的最后一部分,物体碰撞。

在示例游戏中,我们希望创建一套基本的物体碰撞逻辑。如果角色撞上了石头对象,游戏会中止角色的行动并判断其死亡(小朋友们别怕,都是假的)。

我们现在通过向handleComplete函数添加以下代码的方式,将岩石对象添加到stage当中:

case "rock"
    var g = new createjs.Graphics() 
    g.beginBitmapFill(result); 
    g.drawRect(0, 0, 45, 44) 
    rock = new createjs.Shape(g); 
    rock.y = h - 119; 
    rock.x = w; 
    rock.height = 44; 
    rock.width = 45; 
    break
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.

  • 1.

为了在游戏中显示岩石图像,还需要添加以下代码:

stage.addChild(sky, ground, hill, sun, player, rock);  
  • 1.

现在我们的游戏中已经出现了岩石对象,接下来要做的是赋予角色跳跃的能力,从而允许玩家避开岩石。要实现这一目标,我们需要登记stagemousedown事件来捕捉鼠标点击操作,一旦事件被触发、角色就会播放跳跃动画并使isJumping对象为true。

我们将以下代码添加到handleComplete函数当中。

 

stage.addEventListener("stagemousedown"function () { 
   if (isJumping) return
   play("jump_h"); 
   gameOver = false
   isJumping = true
);   
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.

 下一步是在角色跳跃过程中改变其Y值,在本文中我们将采取最简便直接的方式。

代码会首先检查我们的角色当前是否处于空中(以免出现二段跳乃至多段跳情况),然后启动一个计时器。在1秒之后,计时器将执行完毕,这时角色也就回到地面。但整个跳跃过程中角色的Y值将增加4个像素,即角色在空中达到的最大高度为地面Y值再加4个像素。

unction handleJump() { 
    if (isJumping) { 
        if (onTheAir == null) { 
            onTheAir = setTimeout(function () { 
                isJumping = false
                player.y = playerBaseY; 
                onTheAir = null
                goingDown = false
                top = false
            }, 1000); 
        } 
        if (goingDown && player.y <= playerBaseY) { 
            player.y += 4; 
        } 
        else { 
            player.y -= 4; 
            if (player.y <= maxJumpHeight) 
                goingDown = true
        } 
    } 
}   
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.

 要调用这段代码,我们需要向tick函数中添加如下内容:

function tick() { 
    handleJump(); 
    ... 
}   
  • 1.
  • 2.
  • 3.
  • 4.

 

 现在我们只要点击鼠标左键就能让角色向空中跳跃。

最后一项工作是处理对象之间的碰撞事件。如果角色触碰到了岩石,我们需要中止游戏并播放fall动画。我建议大家点击此处阅读并了解各种不同的物体碰撞实现方式。

function checkRectIntersection(r1, r2) { 
    var deltax = r1.x - r2.x; 
    var deltay = r1.y - r2.y; 
    var dist = 25; 
    if (Math.abs(deltax) < dist && Math.abs(deltay) < dist) { 
        if (Math.sqrt(deltax * deltax + deltay * deltay) < dist) { 
            return true
        } 
    } 
    return false
}  
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.

在添加了HandleCollision函数后,tick函数会在每次碰撞检查时对其进行调用

function HandleCollisions() { 
    var a = getCollideableItemBounds(player); 
    var b = getCollideableItemBounds(rock); 
 
    var oppss = checkRectIntersection(a, b); 
 
    if (oppss && !gameOver) { 
        console.log(oppss); 
        gameOver = true
        play("fall_h"); 
    } 
}  
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.

 

点击此处查看最终演示效果。

 

 

 

 

 

原文链接:http://1lastletter.com/CodeProject/index.html

 

 

 

责任编辑:陈四芳 来源: 51CTO
相关推荐

2013-05-20 15:42:22

2013-04-18 11:01:10

手机游戏手机游戏引擎技术选型

2013-05-21 11:26:49

Android游戏开发Sensor感应

2013-05-21 09:56:15

2012-08-09 08:49:30

CoronaCorona SDKCorona SDK游

2011-08-12 08:56:31

JavaScript

2019-02-21 13:40:35

Javascript面试前端

2022-01-21 21:33:03

开发JavaScript应用

2011-08-11 09:16:50

JavaScript

2013-05-20 17:51:47

Android游戏开发SurfaceView

2011-04-25 08:53:47

JavaScript框架

2012-08-10 09:22:38

CoronaCorona SDKCorona SDK游

2013-05-20 17:13:17

Android游戏开发CanvasPaint

2009-06-11 09:19:38

netbeans实例J2ME游戏

2010-08-10 09:11:12

Windows PhoNXA

2012-12-13 13:27:29

Corona SDK

2017-01-12 14:55:50

JavaScript编程

2016-04-25 10:23:52

2020-05-11 09:54:33

JavaScript开发技术

2011-07-27 17:07:06

iPhone 游戏 Cocos2d
点赞
收藏

51CTO技术栈公众号