首页 前端知识 EnchantJS HTML5 游戏编程教程(二)

EnchantJS HTML5 游戏编程教程(二)

2025-02-24 13:02:56 前端知识 前端哥 934 790 我要收藏

原文:HTML5 Game Programming with enchant.js

协议:CC BY-NC-SA 4.0

四、enchant.js 的高级功能

enchant.js 的基础允许简单的游戏创建。更高级的功能,如在单个游戏中的场景之间导航,需要使用 enchant.js 更复杂的功能。在本章中,我们将演示如何创建场景过渡,以包括多个级别或环境,在屏幕上开始和游戏以美化游戏外观,以及地图和声音以增加交互性。

汇总列表

  1. 场景之间的转换
  2. 创建开始、游戏结束和分数共享屏幕
  3. 使用碰撞检测
  4. 创建交互式地图

场景之间的过渡

虽然单个场景对于简单的游戏来说可能已经足够了,但是任何类型的冒险或基于对话的游戏都需要多个场景来保持适合类型的感觉。

本节的示例代码展示了如何创建一个简单的程序,该程序使用场景转换使玩家能够在三个不同的场景之间切换。图 4-1 说明了玩家如何通过点击橙色导航文本在三个场景之间导航。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 4-1 。场景转换

解剖一个场景

场景可能是一个挑战,因为它们有自己的结构,不同于 enchant.js 中的其他实体。因此,我们在本节中为您提供一些关于场景如何工作的基本信息,然后是示例代码,以显示场景如何在实际的 enchant.js 游戏中使用。

场景创建

场景是一个屏幕单元,可以在其中添加显示对象,如精灵、标签、贴图和组。一个游戏可以有多个场景,通过切换场景,可以完全改变画面的内容。常见的场景类型包括标题屏幕、播放屏幕和游戏结束屏幕。为了创建场景,使用了Scene对象构造函数(var scene1 = new Scene();)。

场景堆栈

创建场景后,可以将背景、带文本的标签和精灵添加到场景中。一旦完成,用addChild()函数将场景添加到场景堆栈中。

场景以堆栈结构组织。正如术语“堆叠”所暗示的,多个场景堆叠在彼此之上。最上面的场景是可见场景。你用push()方法在栈顶添加一个场景,用pop()方法移除最顶层的场景,如图图 4-2 所示。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 4-2 。烟囱建造

场景属性和方法

在一个游戏内部,可以用currentScene引用当前场景,用rootScene引用根场景。这两个名字使得跟踪游戏场景堆栈中发生的事情变得容易。

表 4-1 显示了Scene对象的方法。

表 4-1。场景对象方法

代码(参数)描述
pushScene(Scene)切换到新场景,将其添加到场景堆栈的顶部。
popScene()通过将当前场景从屏幕堆栈中移除来结束当前场景。底层场景(如果有)将成为新的当前场景。
removeScene(scene)从堆栈中移除指定的场景。
replaceScene(scene)用另一个场景替换当前场景。

进行场景转换

要创建允许玩家在三个场景之间切换的代码,请执行以下操作:

  1. Fork the code from http://code.9leap.net/codes/show/27650. If you cannot access the code, copy it from Listing 4-1.

    清单 4-1。 场景设置

    enchant();
    window.onload = function() {
        //Core object creation
        var game = new Core(320, 320);
        game.fps = 16;
    
        //Image loading
        game.preload('http://enchantjs.com/assets/img/bg/bg01.jpg',
            'http://enchantjs.com/assets/img/bg/bg02.jpg',
            'http://enchantjs.com/assets/img/bg/bg03.jpg');
    
        //Called when pre-loading is complete
        game.onload = function() {
    
            //Background creation
            var bg = makeBackground(game.assets        ['http://enchantjs.com/assets/img/bg/bg01.jpg'])
            game.rootScene.addChild(bg);
    
            //Message creation
            game.rootScene.addChild(makeMessage("This is the root scene."));
    
            //Choice button creation
            var select=makeSelect("[Move to Scene 1]", 320 - 32 * 2);
            select.addEventListener(Event.TOUCH_START, function(e) {
                game.pushScene(game.makeScene1());
            });
            game.rootScene.addChild(select);
        };
    
        //Scene 1 creation
        game.makeScene1 = function() {
            var scene = new Scene();
    
            //Background creation
            var bg = makeBackground(game.assets        ['http://enchantjs.com/assets/img/bg/bg02.jpg'])
            scene.addChild(bg);
    
            //Message creation
            scene.addChild(makeMessage("This is Scene 1."));
    
            //Choice button creation
            var select = makeSelect("[Move to Scene 2]", 320 - 32 * 2);
            select.addEventListener(Event.TOUCH_START, function(e) {
                game.pushScene(game.makeScene2());
            });
            scene.addChild(select);
            scene.addChild(makeReturn(1));
            return scene;
        };
    
        //Scene 2 creation
        game.makeScene2 = function() {
            var scene = new Scene();
    
            //Background creation
            var bg = makeBackground(game.assets        ['http://enchantjs.com/assets/img/bg/bg03.jpg'])
            scene.addChild(bg);
    
            //Label creation
            scene.addChild(makeMessage("This is Scene 2."));
            scene.addChild(makeReturn(0));
            return scene;
        };
    
        //Start game
        game.start();
    };
    
    //Background creation
    function makeBackground(image) {
        var bg = new Sprite(320, 320);
        bg.image = image;
        return bg;
    }
    //Message creation
    function makeMessage(text) {
        var label = new Label(text);
        label.font  = "16px monospace";
        label.color = "rgb(255,255,255)";
        label.backgroundColor = "rgba(0,0,0,0.6)";
        label.y     = 320 - 32 * 3;
        label.width = 320;
        label.height = 32 * 3;
        return label;
    }
    
    //Choice button creation
    function makeSelect(text, y) {
        var label = new Label(text);
        label.font  = "16px monospace";
        label.color = "rgb(255,200,0)";
        label.y     = y;
        label.width = 320;
        return label;
    }
    
    //Return button creation
    function makeReturn(lineNumber) {
        var game = enchant.Game.instance;
        var returnLabel = makeSelect("[Return]", 320 - 32 * (2-lineNumber));
        returnLabel.addEventListener(Event.TOUCH_START, function(e) {
            game.popScene();
        });
        return returnLabel;
    }
    

在这个程序中,当玩家点击移动到场景 1 按钮时,游戏显示场景 1。方法 pushScene()应该有道理,但是game.pushScene(game.makeScene1());呢?

在前面的示例代码中,我们为我们的Core对象类显式定义了makeScene1()makeScene2()方法。当调用这些方法时,它们会创建并返回一个场景。

该函数中所发生的只是创建一个Scene对象,然后创建各种元素并添加到场景中。首先,代码创建一个背景并将其添加到场景中。接下来,代码创建一个标签并添加到场景中,特别是使用我们已经创建的makeMessage()函数。然后,代码用addChild().将它添加到场景中。之后,代码用同样的方法创建另一个标签,这允许导航到场景 2。

该方法的最后一行返回场景。请记住,向场景添加元素实际上并不会使场景活跃,这就是为什么这里有这个返回。这样做允许我们将它放在pushScene()函数中,该函数将调用makeScene1(),然后使用它创建的场景,将它推到堆栈的顶部。

当玩家在场景 1 或场景 2 中点击返回按钮时,场景堆栈顶部的场景将通过popScene()移除。

场景允许多个环境存在于同一个游戏中。您可以在基于对话框的游戏中使用它们来设置关卡或图像。然而,你不一定要把它们包含在你的游戏中。enchant.js 中很多流行的游戏都是在没有场景具体使用的情况下创建的。

创建一个有屏幕、时间限制和分数 的游戏

开始和游戏结束屏幕是向玩家传达游戏开始和结束的有用工具。幸运的是,enchant.js 附带了一个官方插件 nineleap.enchant.js,用于使这些屏幕易于实现。

在本节的示例代码中,我们创建了一个以开始屏幕开始的游戏。当玩家点击开始屏幕时,会切换到根场景。在根场景中,玩家可以使用 d-pad 来控制熊的左右移动,以接住从屏幕顶部落下的苹果。收集苹果会增加玩家的分数。达到 10 秒钟的时间限制后,游戏停止,并显示游戏结束屏幕。图 4-3 显示了屏幕转换的顺序。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 4-3 。nineleap 示例项目代码的屏幕序列

如果你上传你的游戏到 9leap.net,游戏结束后会出现另一个显示玩家分数的屏幕。

设置一个示例游戏

我们首先需要一个游戏来实现开始和结束屏幕。要创建此游戏,请执行以下操作:

  1. Fork the code from http://code.9leap.net/codes/show/23342 to your own project on code.9leap.net. If you are not using code.9leap.net, copy and paste the code from Listing 4-2. This code is for a simple apple-catching game.

    清单 4-2。 抓苹果游戏

    enchant();
    window.onload = function() {
        //Game object creation
        var game = new Core(320, 320);
        game.fps = 16;
        game.score = 0;
        var label;
        var bear;
    
        //Image loading
        game.preload('chara1.gif',
            'http://enchantjs.com/assets/img/map0.gif',
            'http://enchantjs.com/assets/img/icon0.gif');
    
        //Called when the loading is complete
        game.onload = function() {
            //Background creation
            var bg = new Sprite(320, 320);
            bg.backgroundColor = "rgb(0, 200, 255)";
            var maptip = game.assets['http://enchantjs.com/assets/img/map0.gif'];
            var image = new Surface(320, 320);
            for (var i = 0; i < 320; i += 16) {
                image.draw(maptip, 7 * 16, 0, 16, 16, i, 320 - 16, 16, 16);
            }
            bg.image = image;
            game.rootScene.addChild(bg);
    
            //Virtual pad creation
            var pad = new Pad();
            pad.x   = 0;
            pad.y   = 220;
            game.rootScene.addChild(pad);
    
            //Label creation
            label = new Label("");
            game.rootScene.addChild(label);
    
            //Bear creation
            bear = new Sprite(32, 32);
            bear.image  = game.assets['chara1.gif'];
            bear.x      = 160 - 16;
            bear.y      = 320 - 16 - 32;
            bear.anim   = [10, 11, 10, 12];
            bear.frame  = bear.anim[0];
            game.rootScene.addChild(bear);
    
            //Periodic processing of the bear sprite
            bear.addEventListener(Event.ENTER_FRAME, function() {
                //Left
                if (game.input.left)  {
                    bear.x -= 3;
                    bear.scaleX = -1;
                }
                //Right
                else if (game.input.right) {
                    bear.x += 3;
                    bear.scaleX =  1;
                }
    
                //Frame settings
                if (!game.input.left && !game.input.right) {
                    bear.frame = bear.anim[0];
                } else {
                    bear.frame = bear.anim[bear.age %  bear.anim.length];
                }
            });
        };
    
        //Adds an apple
        game.addApple = function(x, speed) {
            //Create apple
            var apple = new Sprite(16, 16);
            apple.image = game.assets['http://enchantjs.com/assets/img/icon0.gif'];
            apple.x = x;
            apple.y = -16;
            apple.frame = 15;
            apple.speed = speed;
            game.rootScene.addChild(apple);
    
            //Periodic processing of the sprite
            apple.addEventListener(Event.ENTER_FRAME, function() {
                apple.y += apple.speed;
                //Collision with the bear
    
                //Collision with the ground
                else if (apple.y > 320 - 32) {
                    game.rootScene.removeChild(apple);
                }
            });
        };
    
        //Periodic processing of the scene
        game.framesLeft = 10*game.fps; // 10 seconds
        game.rootScene.addEventListener(Event.ENTER_FRAME, function() {
            game.framesLeft--;
            if (game.framesLeft > 0) {
                //Apple creation
                if ((game.frame % 10) === 0) {
                    var x     = rand(300);
                    var speed = 3 + rand(6);
                    game.addApple(x,speed);
                }
                label.text = "Time left:" + Math.floor(game.framesLeft / game.fps)  +
                    "<BR />Score:" + game.score;
            } else {
                //Display Game Over
                game.end(game.score, "Your score is " + game.score);
            }
        });
    
        //Start game
        game.start();
    };
    
    //Generates a random number
    function rand(num){
        return Math.floor(Math.random() * num);
    }
    

如果你运行这段代码,你可以用 d-pad 在屏幕上移动一只熊,然后看到苹果从屏幕顶部落下。然而,开始和游戏结束屏幕还没有出现。在下一节中,我们将向您展示如何添加这些屏幕。

添加所需的插件

随着 nineleap.enchant.js 插件的添加,开始和游戏结束屏幕会自动出现。执行以下操作将其添加到项目中:

  1. 点击屏幕顶部的下拉菜单,选择index.html

  2. Type in the code in Listing 4-3 under <script src='/static/enchant.js-latest/enchant.js'></script>.

    清单 4-3。 添加 nine leap . ench . js 插件

    <script src='/static/enchant.js-latest/plugins/nineleap.enchant.js'></script>
    
  3. 单击运行。由于添加了插件,开始和游戏结束屏幕会自动出现。

创建分数共享屏幕

只有当你将游戏上传到 9leap.net 时,分数共享界面才会出现。这个屏幕允许玩家看到他们的最终分数,并让他们能够通过 9leap.net 在 Twitter 上分享分数。要设置游戏使用分数共享屏幕,请执行以下操作:

  1. Near the end of the code, change game.end(); to what is shown in Listing 4-4.

    清单 4-4。 使用计分屏

    game.end(game.score, "Your score is " + game.score);
    

接下来,把你的游戏上传到 9leap.net。当你玩游戏时,游戏结束后会出现一个屏幕,显示你的分数和你指定的信息(如“你的分数是…”)。

正如你所看到的,开始和游戏结束屏幕很容易在你的游戏中实现,只需要一个额外的插件。添加这个插件可以润色你的游戏,让你的玩家更容易理解确切的起点和终点。

使用碰撞检测

正如我们已经看到的,我们的游戏包括让一个熊角色收集从屏幕顶部落下的苹果。就苹果而言,我们知道如何创造它们,如何让它们随着时间的推移而掉落,甚至如何让它们消失。然而,我们需要一些方法来判断我们的熊角色何时接触到苹果。为此,我们使用特定的方法,这些方法是所有实体对象的一部分。

碰撞检测的方法

表 4-2 显示了用于碰撞检测的两种方法。这些方法是Entity对象的一部分,因此可以在场景中的任何地方调用。

表 4-2。碰撞检测方法

方法(参数)描述
intersect(otherEntity)根据实体(调用intersect()的实体和另一个实体)的边界矩形是否相交来执行碰撞检测。另一个实体必须具有属性xywidthheight
within(otherEntity, distance)根据两个实体中心点之间的距离执行碰撞检测。

用内部方法检测碰撞

第一种碰撞检测方法是within()。在抓苹果游戏中执行以下操作:

  1. Beneath the line //Collision with the bear, type in the code in Listing 4-4.

    清单 4-4。 利用内()

    if (bear.within(apple, 16)) {
        game.score+=30;
        game.rootScene.removeChild(apple);
    }
    

这将计算每一帧苹果的中心点和熊的中心点,然后计算两者之间的距离。如果距离小于或等于 16 个像素,游戏分数增加 30,苹果从rootScene中移除。

用相交方法检测碰撞

存在另一种检测物体之间碰撞的方法。请执行以下操作,了解如何使用该方法:

  1. 将清单 4-4 中的bear.within(苹果,16)替换为bear.intersect(苹果)。

这导致程序查看熊和苹果的总面积。如果这些区域相交,分数增加 30,苹果从rootScene中移除。

碰撞检测是游戏的重要组成部分。抓苹果、击落船只或任何涉及一个精灵与另一个精灵接触的事情都需要碰撞检测。如果您在创建本节中的代码时遇到任何问题,您可以在http://code.9leap.net/codes/show/27891找到一个完整的工作副本。

创建交互式地图

我们在第三章的中看到了一个地图的基本示例,它在屏幕上复制了一个单幅图块,但需要进一步设置才能在 enchant.js 中创建交互式地图。然而,原理是相同的,仍然使用draw()方法用地图图块平铺给定的表面。

在本节的示例代码中,我们使用地图创建了一个简单的迷宫程序。我们的主角将只能穿过浅棕色的路面砖,而不能走过绿色的草地砖。一旦主角到达宝箱,游戏就结束了。图 4-4 和 4-5 说明了该程序。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 4-4 。地图示例项目的开始屏幕

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 4-5 。地图示例项目的结束画面

创建地图对象

创建地图的第一步是创建一个Map对象。执行以下操作来设置游戏并创建一个Map对象:

  • 1.从http://code.9leap.net/codes/show/27204派生出空白模板,以获得一个项目来输入您的代码。

  • 2.  Type in the code in Listing 4-5 to set up the basics of the game.

    清单 4-5。 侧滚器基础知识

    var DIR_LEFT  = 0;
    var DIR_RIGHT = 1;
    var DIR_UP    = 2;
    var DIR_DOWN  = 3;
    
    enchant();
    window.onload = function() {
        var game = new Core(320, 320);
        game.fps = 16;
        game.preload(
            'http://enchantjs.com/assets/img/map0.gif',
            'http://enchantjs.com/assets/img/chara0.gif');
    
        game.onload = function() {
            var player = new Sprite(32, 32);
            player.image = game.assets['http://enchantjs.com/assets/img/chara0.gif'];
            player.x = 2 * 16;
            player.y = 16;
            player.dir   = DIR_DOWN;
            player.anim  = [
                 9, 10, 11, 10, //Left
                18, 19, 20, 19, //Right
                27, 28, 29, 28, //Up
                 0,  1,  2,  1];//Down
    
                //Frame setting
                if (!game.input.up && !game.input.down &&
                    !game.input.left && !game.input.right) player.age = 1;//Standing Still
                player.frame = player.anim[player.dir * 4 + (player.age % 4)];
    
            });
    
            var pad = new Pad();
            pad.y = 220;
            game.rootScene.addChild(pad);
    
        };
        game.start();
    };
    
  • 3.  Under game.onload = function() {, type in the code in Listing 4-6 to create the Map object and assign the tile set image to it.

    清单 4-6。 创建地图对象

    var map = new Map(16, 16);
    map.image = game.assets['http://enchantjs.com/assets/img/map0.gif'];
    

填充图块并设置碰撞和

现在您已经创建了地图,您需要用切片填充它。在前面的例子中,我们使用了一个循环来用相同的绿草瓦片填充地图中的所有瓦片。在此地图中,我们以特定的排列方式使用不同的图块,因此必须手动输入。执行以下操作来复制和粘贴图块数据:

  • 4.  Copy the code from http://code.9leap.net/codes/show/27905 and insert it into the line after Listing 4-6. If you are not using 9leap, copy the code from Listing 4-7.

    清单 4-7。 载入磁贴数据

     map.loadData([
    [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
    [0,2,2,2,2,0,0,0,0,0,0,0,0,0,0,0,0,0,2,2,2,2,0],
    [0,2,2,2,2,0,0,2,2,2,2,2,2,2,2,0,0,0,2,2,2,2,0],
    [0,2,2,2,2,0,0,2,2,2,2,2,2,2,2,0,0,0,2,2,2,2,0],
    [0,0,2,2,0,0,0,2,2,0,0,0,0,2,2,0,0,0,0,2,2,0,0],
    [0,0,2,2,0,0,0,2,2,0,0,0,0,2,2,0,0,0,0,2,2,0,0],
    [0,0,2,2,0,0,0,2,2,0,0,0,0,0,0,0,0,2,2,2,2,0,0],
    [0,0,2,2,2,2,2,2,2,2,2,2,2,2,0,0,0,2,2,2,2,0,0],
    [0,0,2,2,2,2,2,2,2,2,2,2,2,2,0,0,0,2,2,0,0,0,0],
    [0,0,0,0,0,2,2,0,0,0,0,0,2,2,2,2,2,2,2,0,0,0,0],
    [0,0,0,0,0,2,2,0,0,0,0,0,2,2,2,2,2,2,2,0,0,0,0],
    [0,0,0,2,2,2,2,0,0,0,0,0,2,2,0,0,0,0,0,0,0,0,0],
    [0,0,2,2,2,2,2,0,0,0,0,0,2,2,2,2,2,2,2,2,2,0,0],
    [0,0,2,2,2,0,0,0,0,0,0,0,2,2,2,2,2,2,2,2,2,0,0],
    [0,0,2,2,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,2,0,0],
    [0,0,0,2,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,2,0,0],
    [0,0,0,2,2,0,0,0,0,0,0,0,0,0,0,0,0,0,2,2,2,2,0],
    [0,0,0,2,2,2,2,2,2,2,2,2,2,2,2,2,0,0,2,2,2,2,0],
    [0,0,0,2,2,2,2,2,2,2,2,2,2,2,2,2,0,0,2,2,2,2,0],
    [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]
    ],[
    [-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
    [-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
    [-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,18,-1,-1,-1],
    [-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
    [-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
    [-1,-1,-1,-1,-1,23,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
    [-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
    [-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,23,-1,-1,-1,-1,-1,-1,23],
    [-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,23],
    [-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
    [-1,-1,-1,-1,-1,-1,-1,-1, 1, 1, 1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
    [-1,-1,-1,-1,-1,-1,-1,-1, 1, 1, 1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
    [-1,-1,-1,-1,-1,-1,-1,-1, 1, 1, 1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
    [-1,-1,-1,18,-1,-1,-1,-1,-1, 1, 1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
    [-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
    [-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
    [-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
    [-1,23,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,25,-1,-1],
    [-1,23,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
    [-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1]
    ]);
    

这段代码展示了如何使用loadData()函数来填充地图。它由两部分数组组成,由一个右括号、一个逗号和一个左括号“],[”分隔。因为这是两个独立的组,所以让我们从第一组开始。

[0,0,0,0 . . .]开始的第一个数组组指定在第 1 个第 1 个位置(绿草瓷砖)开始布局瓷砖,作为瓷砖地图的第一行。这些图块来自图块集图像,该图像先前被分配给map.image。使用的图块大小(16x16)是在第一次用Map对象构造器(var map = new Map(16, 16);)创建地图时指定的。

第一行图块名称后的右括号和逗号(…2,2,2,0),)指定开始新的一行图块。这个平铺过程一直持续到我们到达清单 4-10 中的[大约一半的位置],

然后我们再次开始这个过程。但是,这一次,指定的图块将覆盖当前位于指定要平铺的位置的任何图块。值 1 表示不在指定位置放置任何单幅图块。这种分层技术允许将诸如花、树和宝箱之类的物体放在地图上,放在绿草地或褐色的路面砖上。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 注意手动输入定义地图平铺顺序的二维数组或用于碰撞检测的二维数组是相当费力的。为了自动创建这些数据,enchant.js 支持地图编辑器,我们建议您在为游戏创建地图时使用该编辑器。你可以在http://enchantjs.com/resource/the-map-editor/找到地图编辑器和使用说明。

下一步是在地图上指定碰撞点。在地图上,既会有绿色的草地瓦片,也会有棕色的道路瓦片。主角应该只能在棕色的路面砖上行走,所以我们设置了碰撞来实现这一点。这是用另一个数组设置的,用0表示可以通行,1表示不可以通行,指定了角色是否可以在瓷砖上行走。

  • 5.  Create the collision data for the map by copying it from http://code.9leap.net/codes/show/27909 and pasting it beneath the loadData array you just entered. You can also copy it from Listing 4-8 if necessary.

    清单 4-8。 设置碰撞数据

    map.collisionData = [
        [1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1],
        [1,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,1],
        [1,0,0,0,0,1,1,0,0,0,0,0,0,0,0,1,1,1,0,0,0,0,1],
        [1,0,0,0,0,1,1,0,0,0,0,0,0,0,0,1,1,1,0,0,0,0,1],
        [1,1,0,0,1,1,1,0,0,1,1,1,1,0,0,1,1,1,1,0,0,1,1],
        [1,1,0,0,1,1,1,0,0,1,1,1,1,0,0,1,1,1,1,0,0,1,1],
        [1,1,0,0,1,1,1,0,0,1,1,1,1,1,1,1,1,0,0,0,0,1,1],
        [1,1,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,0,0,0,0,1,1],
        [1,1,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,0,0,1,1,1,1],
        [1,1,1,1,1,0,0,1,1,1,1,1,0,0,0,0,0,0,0,1,1,1,1],
        [1,1,1,1,1,0,0,1,1,1,1,1,0,0,0,0,0,0,0,1,1,1,1],
        [1,1,1,0,0,0,0,1,1,1,1,1,0,0,1,1,1,1,1,1,1,1,1],
        [1,1,0,0,0,0,0,1,1,1,1,1,0,0,0,0,0,0,0,0,0,1,1],
        [1,1,0,0,0,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,1,1],
        [1,1,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,1,1],
        [1,1,1,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,1,1],
        [1,1,1,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,1],
        [1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,0,0,0,0,1],
        [1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,0,0,0,0,1],
        [1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1]
    ];
    

使用运动碰撞检测

现在我们游戏中的地图存在了,但是我们需要一种方法来根据数据移动我们的角色。enchant.js 中的地图支持查找地图上特定位置是否可以行走的方法。这用于确定角色是否可以在瓷砖上行走。执行以下操作来实现它:

  1. Under the line player.anim  = [...];//Down, type in the code in Listing 4-9.

    清单 4-9。 人物移动与地图碰撞检测

    player.addEventListener(Event.ENTER_FRAME, function() {
        //Move up
        if (game.input.up) {
            player.dir = DIR_UP;
            player.y -= 4;
            if (map.hitTest(player.x + 16, player.y + 32)) player.y += 4;
        }
        //Move down
        else if (game.input.down) {
            player.dir = DIR_DOWN;
            player.y += 4;
            if (map.hitTest(player.x + 16, player.y + 32)) player.y -= 4;
        }
        //Move left
        else if (game.input.left) {
            player.dir = DIR_LEFT;
            player.x -= 4;
            if (map.hitTest(player.x + 16, player.y + 32)) player.x += 4;
        }
        //Move right
        else if (game.input.right) {
            player.dir = DIR_RIGHT;
            player.x += 4;
            if (map.hitTest(player.x + 16, player.y + 32)) player.x -= 4;
        }
    
        //Frame setting
        if (!game.input.up && !game.input.down &&
            !game.input.left && !game.input.right) player.age = 1;//Standing Still
        player.frame = player.anim[player.dir * 4 + (player.age % 4)];
    
    });
    

hitTest()方法使用我们分配给collisionData的数据阵列来查找地图上给定 XY 坐标处是否存在障碍物。虽然这种方法可以告诉我们在地图上的给定位置是否存在障碍物,但是如果障碍物确实存在,我们必须手动指定应该做什么。

这些代码都在一个定期处理的事件侦听器中,所以它随着每个新帧的出现而执行。玩家角色在 d-pad 指定的方向上移动四个像素。

但是,如果角色的水平中点与包含障碍物的牌相邻,则角色不能在该障碍物的方向上继续移动。你有没有注意到清单 4-12 中嵌套的if语句是如何从移动的相反方向增加角色的位置的?通过使用相反的计算,角色位置的值不会改变,角色保持静止(如果map.hitTest()返回true)。

滚动地图

我们为我们的角色使用的地图比游戏屏幕大,这意味着地图的一部分在屏幕外被遮住了。当角色在屏幕上移动时,地图应该在某个点滚动,这样角色就可以探索整个地图。

要做到这一点,我们需要做的第一件事是将角色和地图合并成一个单元,这样两者可以同时滚动。为此,请执行以下操作:

  1. Above var pad = new Pad();, combine the map and the player into a single Group by typing in the code in Listing 4-10.

    清单 4-10。 将地图和玩家组合成一组

    var stage = new Group();
    stage.addChild(map);
    stage.addChild(player);
    game.rootScene.addChild(stage);
    
  2. Set the map to scroll with the player by typing in the code in Listing 4-11 after game.rootScene.addChild(pad);.

    清单 4-11。 滚动地图

    game.rootScene.addEventListener(Event.ENTER_FRAME, function(e) {
        //Set stage XY coordinates
        var x = Math.min((game.width  - 16) / 2 - player.x, 0);
        var y = Math.min((game.height - 16) / 2 - player.y, 0);
        x = Math.max(game.width,  x + map.width)  - map.width;
        y = Math.max(game.height, y + map.height) - map.height;
        stage.x = x;
        stage.y = y;
    });
    

如果我们反过来看,这可能是最容易理解的。

首先,我们知道我们正在重新定位上面创建的stage组,通过给舞台的 x 和 y 位置分配一个xy值。记住stage包含了地图和角色,并且会同时移动它们。

接下来,我们指定“从两个值中的一个减去地图的宽度,并将其赋给 x”(x = Math.max(game.width,  x + map.width)  - map.width;)。这里要注意的主要概念是,如果游戏的宽度大于某个其他值(x + map.width),x 会被赋一个值,等于游戏的宽度减去地图的宽度。如果队伍移动了这个数字(一个负数),地图的右边将与游戏屏幕的右边对齐。

最后,直接在事件监听器声明下,我们指定“要么使 x 的值为零,要么等于游戏宽度的一半减去玩家(在地图上)的当前 x 位置。”如果角色在地图上,但没有向右移动至少相当于游戏屏幕一半的量,则分配零分。

这有点复杂,但最终的结果是,地图随着角色的移动以一种总是充满游戏屏幕的方式滚动。在http://9leap.net/games/3004可以看到一个更简单的游戏《Heart_Runner》中滚动背景的例子,玩家快速点击屏幕,使角色跑过一片滚动到角色身后左侧的森林。

对目标使用碰撞检测

在游戏的这一点上,地图和角色应该会正确的出现。这个角色可以用 d-pad 控制,地图会滚动。然而,如果角色走过宝箱,什么也不会发生。要结束游戏,我们必须以不同的方式使用碰撞检测。

我们已经看到了如何在两个实体(intersect()within())之间执行碰撞检测,并且我们已经看到了如何执行基本的碰撞检测来查看角色是否可以在给定的图块上移动。但是,因为宝箱一旦平铺到地图上,就没有自己的实体容器,所以我们不能使用像intersectwithin()这样的实体方法。

无论从哪方面来看,宝箱都只是地图上的一组坐标。要确定角色是否与宝箱接触,我们需要确定角色所在位置与地图上宝箱位置之间的距离。图 4-6 显示了这个等式。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 4-6 。距离公式

请执行以下操作,了解如何创建和使用此公式:

  1. Add the function to calculate the distance between two points on the map by typing in the code in Listing 4-12 at the very end of your existing code, outside the game.onload function.

    清单 4-12。 创建函数计算长度

    function calcLen(x0, y0, x1, y1) {
        return Math.sqrt((x0 - x1) * (x0 - x1) + (y0 - y1) * (y0 - y1));
    }
    

    函数Math.sqrt()计算作为参数传递的任何值的平方根,它是Math对象的函数,可以在 JavaScript 中随时调用。

  2. Check if the character is touching the treasure chest panel by typing in the code in Listing 4-13 after player.frame = player.anim[player.dir * 4 + (player.age % 4)];.

    清单 4-13。 检查与宝箱的碰撞

    if (calcLen(player.x + 16, player.y + 16, 20 * 16 + 8, 17 * 16 + 8) <= 16) {
        game.end(0, "Goal");
    }
    

    在这里,我们正在测量玩家精灵的 XY 坐标与宝箱之间的距离。“+16”和“+8”是为了让程序根据这两个对象的中心而不是左上角来测量它们之间的距离。

    用于藏宝箱的 XY 位置(x1 和 y1)的乘法使用单个区块的大小(16 像素)乘以藏宝箱区块之前的区块数,来找到 x 和 y 位置的值。

  3. 单击运行。当角色接触到宝箱时,游戏结束。

在本节中,我们创建了一个游戏,使角色能够在地图上行走,但只能在特定的瓷砖上行走。我们还让地图随着角色的移动而滚动。最后,我们让游戏在角色到达地图上的宝箱后结束。所有这些功能和动作都涉及到交互式地图。互动地图非常适合二维游戏,尤其是二维角色扮演游戏。

如果您在设置本节中的地图时遇到任何问题,您可以在http://code.9leap.net/codes/show/23344找到完整的工作代码示例。

实现声音

本章的最后一个高级功能是声音。声音可以在 enchant.js 中实现,用于游戏中的背景音乐和事件,例如一艘船射击,一个特殊的能力被激活,等等,以使你的游戏身临其境,具有交互性。

本节的练习只是在屏幕上放几个香蕉,以及一个头骨。如果玩家点击一个香蕉,香蕉就会消失,并播放声音效果。如果玩家点击头骨,游戏将结束。游戏的目标是收集所有的香蕉。图 4-7 显示了这个简单游戏的开始状态。图 4-8 显示游戏画面。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 4-7 。香蕉游戏主屏幕

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 4-8 。香蕉游戏结束画面

下载声音

由于声音文件很大,默认情况下它们不包含在 enchant.js 包中,但是可以从同一个页面下载。执行以下操作来下载它们:

  1. 转到http://enchantjs.com并点击下载。从页面底部的链接下载包含所有声音的 zip 文件。
  2. 播放一些声音,看看文件中包含了什么。

设置游戏

对于这个例子,我们将复制一个完整游戏的代码,然后在其中实现声音。执行以下操作来设置游戏:

  1. Fork the code at http://code.9leap.net/codes/show/27917 for the complete game. If you are not using 9leap.net, copy and paste the code from Listing 4-14 into a blank enchant.js game.

    清单 4-14。 香蕉游戏

    enchant();
    window.onload = function() {
        //Game object creation
        var game = new Core(320, 320);
        game.fps = 16;
        game.score = 0;
        game.bananaNum = 10;
        game.time = 0;
    
        //Sound and image loading
        game.preload(['se1.wav',
                      'http://enchantjs.com/assets/img/icon0.gif',
                      'http://enchantjs.com/assets/img/map0.gif']);
    
        //Called when the loading is complete
        game.onload = function() {
    
            //Background creation
            var bg = new Sprite(320, 320);
            var maptip = game.assets['http://enchantjs.com/assets/img/map0.gif'];
            var image = new Surface(320, 320);
            for (var j = 0; j < 320; j += 16) {
                for (var i = 0; i < 320; i += 16) {
                    image.draw(maptip, 16 * 2, 0, 16, 16, i, j, 16, 16);
                }
            }
            bg.image = image;
            game.rootScene.addChild(bg);
    
            //Add bananas
            for (var k = 0; k < 10; k++) game.addBanana();
    
            //Add skull
            game.addDokuro();
    
            //Periodic scene processing
            game.rootScene.addEventListener(Event.ENTER_FRAME, function(){
                game.time ++;
            });
        };
    
        //Adds a skull
        game.addDokuro = function(){
            var dokuro = new Sprite(16, 16);
            dokuro.x = rand(260) + 20;
            dokuro.y = rand(260) + 20;
            dokuro.image = game.assets['http://enchantjs.com/assets/img/icon0.gif'];
            dokuro.frame = 11;
            dokuro.addEventListener(Event.TOUCH_START, function(e) {
                game.end(0, "Game Over");
            });
            game.rootScene.addChild(dokuro);
        };
    
        //Adds a banana
        game.addBanana = function(){
            var banana = new Sprite(16, 16);
            banana.x = rand(260) + 20;
            banana.y = rand(260) + 20;
            banana.image = game.assets['http://enchantjs.com/assets/img/icon0.gif'];
            banana.frame = 16;
    
            //Event handling when the banana is touched
            banana.addEventListener(Event.TOUCH_START, function(e) {
                game.rootScene.removeChild(this);
    
                game.bananaNum--;
                if (game.bananaNum === 0){
                    game.end(1000000 - game.time,
                        (game.time / game.fps).toFixed(2) + " seconds to Clear!");
                }
            });
            game.rootScene.addChild(banana);
        };
    
        //Start game
        game.start();
    };
    
    //Generates a random number
    function rand(num){
        return Math.floor(Math.random() * num);
    }
    

装载声音

就像处理图像一样,你可以通过使用Core对象的preload()函数将声音加载到内存中。尽管Sound对象支持用load()方法直接加载,我们还是建议用preload()方法加载你的声音元素,因为这样可以避免玩家在游戏中等待元素加载的问题,如果他们的连接速度很慢的话。

一旦你的元素被预加载,它们可以通过使用Sound对象构造函数在你的游戏中被创建为声音对象。请执行以下操作来了解如何操作:

  1. After game.onLoad = function() {, type in the code in Listing 4-15.

    清单 4-15。 创建声音对象

    game.se = game.assets['se1.wav'];
    

播放声音

一旦创建了声音对象,就可以用特定的方法来播放它:

  1. After game.rootScene.removeChild(this);, type in the code in Listing 4-16.

    清单 4-16。 奏出声音

    game.se.play();
    
  2. 单击运行。当你点击香蕉时,会有声音播放。点击头骨结束游戏。

在本节中,我们创建了一个游戏,当玩家点击游戏中的香蕉时,游戏会发出声音。声音为游戏增加了互动元素,为玩家提供了额外的感官反馈元素。利用 enchant.js 主页上提供的免费声音库,很容易在游戏中加入声音。

结论

祝贺您通过了高级部分!您现在应该了解了 enchant.js 更高级的功能,包括如何在游戏中组织和导航场景,如何在屏幕上创建开始和游戏,如何实现地图和滚动地图,以及如何启用声音播放。这将允许你在游戏中增加更多的互动,让你的游戏更吸引玩家。

在下一章,我们从 enchant.js 的具体功能上退一步,看看游戏设计。制作自己的原创游戏需要创造力,因此我们向您介绍一个示例工作流,您可以使用它来创建自己的游戏,带您完成设计过程的每一步。

五、游戏设计

到目前为止,我们已经专注于 JavaScript 和 enchant.js 的基础知识,包括您必须用来编写代码以利用 enchant.js 的许多功能的特定格式。这些基础知识包括精灵、标签、场景、地图、如何在9leap.net上上传和共享您的游戏,等等。然而,这并不是一个好的游戏开发商所能做到的!

如果你一头扎进去,试图在没有任何参考资料的情况下,仅仅使用我们之前在本书中提到的内容,从头开始构建你自己的游戏,你将会遇到问题。很多问题。为了使整个过程对你来说更容易,我们在这一章中提供了一些关于从头开始设计和开发游戏的背景。

强大的能力(或者说功能)意味着巨大的责任,作为一名游戏开发者,你对你的玩家有责任。enchant.js 引擎是开发游戏的强大工具。但它的最终目的是什么?当然是为了给玩家带来快乐!好玩是游戏的重点。你的玩家是与你的游戏互动的人,如果他们不喜欢这种体验,他们就没有动力玩你花时间开发的游戏。

这一章特别介绍了构建原创游戏的创作过程。它充满了如何使这个过程变得简单的提示。它也给你一些提示,告诉你如何设计你的游戏来吸引和娱乐你的玩家。我们涵盖了一个游戏应该经历的开发周期:从构思,到编码,最后到改进游戏成品。

迷你游戏开发的牢不可破的规则

在用 enchant.js 制作游戏时,有几个“牢不可破”的规则是你应该遵守的,用该库开发的大多数游戏都是迷你游戏。迷你游戏是小规模的游戏,其特点是游戏时间短,关卡之间的进展简单。

通常,它们只需要一到三个人来开发,有时不到一个小时就可以完成。虽然有些是在几天内开发出来的,但是你不应该在一个游戏上花太多的钱。不断尝试,不要担心失败。你犯的错误越多,从中吸取的教训越多,你的技能就会提高得越多。

不要试图制作史诗游戏

如果你试图从一开始就制作一个史诗游戏,你肯定会失败。从制作一个适合你技能的小而简单的游戏开始,然后逐渐扩展它的功能。事实上,之前大部分财务上成功的游戏,并不是以打造一款史诗级游戏为目标开始的。如果你仔细看看那些游戏,你会发现它们都是由极其简单的组件组成的,尽管数量很多。

在一个完整的简单游戏中,存在许多阶段,每个阶段都通过一个故事连接到另一个阶段。从一开始规则就很复杂的游戏很少见。

不要试图做一个完全原创的游戏

重要的是,当你制定游戏规则时,不要试图实现 100%的原创性。游戏已经以这样或那样的形式发展了很长时间。许多初学者开始渴望做一些没有人见过的东西,但他们冒着创造一些事实上没有人喜欢的东西的风险。试着想象一种没人尝过或听说过的食物。现在想象一下吃这样一种食物,由一个从来没有做过饭的人准备的。一个可怕的想法,不是吗?

它可能完全是一种幸运的美味,但是美食通常不会偏离我们习惯听到的描述它们的形容词:甜、辣、多汁、丰盛等等。当然,一个大师级的厨师会用新的和创新的方式来处理这些味道,但是你不会看到那些刚刚开始学习烹饪艺术的人一头扎进烹饪科学的实验中。

创新很重要,但是如果味道很糟糕,那就不值得了。考虑花生酱和果冻。将两种熟悉的味道结合成新的东西比尝试烹饪完全闻所未闻的东西要好。游戏也一样。不要一开始就以原创为目标。结合熟悉的主题和规则,然后逐渐开始创造你自己独特的转折。

玩家要在十秒钟内掌握游戏

人们很容易被有许多规则的复杂游戏吓倒。在一个完美的游戏中,玩家本能地掌握游戏如何运作,并在开始玩的那一刻就被吸引住了。你要让玩家觉得“这很好玩!”在十秒或更短的时间内。不要指望你的球员有耐心。从他们开始玩游戏的那一刻起,他们就应该尽可能多地了解这个游戏及其吸引力。

如果你的游戏有多个关卡或者比一个标准的迷你游戏更复杂,可以考虑在游戏的开头增加一个可选的教程关卡。教程等级是教你的玩家如何玩的好方法,同时保持动手的感觉。

不要太沉迷于编程

编程本身就很有趣,一些游戏拥有迷人的结构。也就是说,人们很容易过度关注编程,而忽略了游戏的本质。请记住,你的最终目标是创造一些很多人会玩的东西。把你的注意力放在让游戏有趣上,而不是复杂的编程上。

遇到麻烦时,就去“时间攻击”

在大多数游戏中,你可以简单地通过增加时间限制(时间攻击)来增加赌注和乐趣。例如,在 9leap ( http://9leap.net/games/4)发布的“高速黑白棋”中,一个简单的黑白棋(奥赛罗)游戏通过获得时间限制获得了新的生命。这是一场慢节奏的战略游戏,极大地增加了赌注和难度。

不要纠结于失去了什么

对于游戏开发者来说,时间和资源短缺是很典型的,即使一个大型游戏是由一家大公司开发的。无限的开发时间和资源几乎是不可能的。当你开始为缺乏时间和资源而烦恼的时候,你的游戏进程就停止了。相反,专注于在时间和资源允许的情况下做出最好的游戏。

让别人开心

创造一个游戏的全部意义在于为他人创造一些乐趣。如果你费尽心思做了一个游戏,不要纠结于有多少人下载它或者它有多精英。试着用你的游戏带给你的朋友和家人一个微笑。

游戏开发流程

游戏无疑有多种形式,当你计划在 enchant.js 中制作自己的游戏时,研究一些不同类型的游戏总是一个好主意,例如 Fruits Panic(在9leap.net/games/90的一种动作益智游戏)或 Golf Solitaire(在9leap.net/games/2994的一种纸牌游戏)来获得灵感。

然而,一旦你决定了你感兴趣的游戏类型,如果你没有一个路线图来指引你正确的方向,你如何着手实际创建它会是一个混乱的过程。下面的列表显示了开发你自己的游戏的六个步骤。请注意,游戏设计是一个非常大的话题,有许多不同的观点。因为用 enchant.js 创建的大多数游戏都很简单,所以这里列出的开发过程可能不同于更复杂的方法。

  1. 设计游戏规则。
    • a.当你刚开始玩你的第一个游戏时,独自从头开始创建所有的规则可能会有点混乱。例如,如果你正在设计一个像“方块断路器”这样的游戏,你需要决定球在撞击方块和撞击球拍时将如何改变方向,决定所有的电源、球的速度、球加速的速率等等。在这种情况下,看看别人是如何为自己的游戏实现规则并模仿他们的技术是一个非常好的主意。在这个过程中,你应该作为模型使用的游戏是你觉得自己能够制作的游戏。把你的想法写在纸上,如果可以的话,甚至模拟一些动作。如果你还不知道所有的细节,不要担心。不要过度。
  2. 选择一个主题并准备精灵。
    • a.选择一个你想做的游戏的大致想法。花一分钟想象你的游戏。会是赛车吗?宇宙飞船与外星人战斗?之后,玩家会对什么念念不忘?你已经决定了你的游戏规则,所以想一个能最大化玩家潜力的主题可能是个好主意。你还应该评估你的艺术,声音和故事资产,以配合主题。你将如何创造或获得它们?
  3. 给游戏编程。
    • a.你已经选好了你的主题和规则,现在是时候根据它们来编写游戏代码了。根据你的主题,在此之前你可能需要准备一些基本的图片、声音或故事。
  4. 自己玩吧。
    • a.一旦你的游戏版本被编程,你必须亲自尝试一下。无论你多么仔细地制定游戏规则,如果你不真正测试游戏,你就会错过规则中的问题。你不仅仅是测试你的游戏是否遵循你已经建立的规则。你也在反复测试平衡,看看游戏是太难,太容易,还是太难控制。如果可以的话,寻求他人的帮助。玩这个游戏的人越多,给你的反馈越多,你就越能学会如何让它变得更有趣。
  5. 完善您的规则并返回到步骤 3。
    • a.在对你的游戏做了一点测试后,重新检查你的规则,看看如何改进它们,修改你的代码来实现改进,然后再玩你的游戏。根据需要重复这个公式。游戏的最终趣味水平将会因你的细心程度而大相径庭。如果你对自己提出的内容感到满意,增加更多的关卡可能是个好主意。
  6. 完成游戏。
    • a.这里是您添加最后润色的地方。比如制作标题画面,添加高分排名表,细化游戏外观等。

如果你已经有了一个你想做的游戏的想法,请随意交换第一步和第二步。首先选择一个主题,然后制定相应的规则,这可能会很有趣。

在实际的游戏开发中,这个过程要复杂得多,并且根据制作过程的不同而不同。然而,由于 enchant.js 提供的功能,开发周期缩短到这样一个程度:与不使用该库相比,您可以更快地重复这个游戏开发周期。

试着想出一个你想开发的游戏。在接下来的部分中,我们将一步一步地介绍开发过程,从头到尾制作一个完整的示例游戏。当我们经历这个过程时,试着找到利用这个过程来开发你自己的游戏的方法。

设计游戏规则

实际上,你可以通过决定游戏规则或者游戏主题来开始设计你的游戏。然而,如果你是游戏开发的初学者,我们建议你首先规划规则,因为执行游戏规则是游戏编程新手的最大障碍。

你可能会想,“你是怎么想出游戏规则的?”好吧,说白了,你先看看外面已经有什么了。挑出与你的概念相似,与你的技能水平相匹配的东西。在短暂但丰富的电脑游戏历史中,你会发现游戏中的每一条规则都被反复实验、润色,并转化为现有的游戏。就像在武术中一样,游戏有一些预先定义好的风格和技巧。在这一点上不要担心创新。

考虑从9leap.net下载一个开源游戏,然后重组源代码,或者检查源代码,并将其作为在自己的游戏中实现类似规则的灵感。不要因为从现有游戏中借用游戏的核心规则而感到难过。即使你一开始就这样做,通过一点一点地给游戏添加新元素,你作为游戏设计师的个性和个性就会开始显现。

打地鼠、射击游戏、基于对话的角色扮演游戏(RPG)和解谜游戏是初学者模仿的一些流行类型。如果你打算专门为智能手机制作一款游戏,我们建议你从拼图或打地鼠游戏开始。

在下面的章节中,我们使用打地鼠作为创建新游戏的基础。如果你碰巧不熟悉,打地鼠是一种游戏,目标是当鼹鼠出现在他们的洞里时击中它们。这是一个简单的游戏,适合初学者。

首先,请访问http://code.9leap.net/codes/show/23694并玩游戏。简要地看一下代码来熟悉它,但是您现在还不需要复制任何东西。

挑选一个主题

一个真正的打地鼠游戏使用一只鼹鼠的图像。然而,让我们使用一个机器人(Android)角色的图像来代替鼹鼠。Droid 图像是在知识共享许可下授权的,所以我们可以随意使用它。

因此,对于这个游戏,我们将制作一个重击机器人游戏,而不是重击鼹鼠游戏。正如你所看到的,在选择了你的规则之后,选择一个主题是绝对没有错的。重要的是从简单的事情开始。

当然,如果你喜欢画画,你可以画一颗痣或者别的什么东西(甚至是某个人)来让自己变得疲惫不堪。仅仅通过改变你的主题,游戏本身的本质就可以发生巨大的变化。即使游戏的逻辑保持不变,通过改变一个小的视觉方面,整个游戏体验可以发生巨大的变化。

选择主题后,在纸上画出游戏的样子。不一定要画得好。当你把游戏的元素写在纸上时,它可以帮助你理解创建游戏所需的步骤。图 5-1 显示了我们的重击机器人游戏的草图。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 5-1 。素描你的游戏

草图显示了一个 3×3 的网格,机器人将从这些网格中出现。随着时间的推移,多个机器人会从洞里出现,当玩家点击它们时,它们会做鬼脸,然后又掉回洞里。每成功击中一次,玩家的分数就会增加。在特定时间或特定数量的机器人出现后,游戏将结束。

当你进入编码阶段时,偏离这个草图并提供一个解释是非常好的,但是把一些东西写在纸上给你一个工作的起点。在此之后,如果有必要,您可以继续创建您的图像。

为了制作 Droid 的图像文件(图 5-2 ),我们使用了免费的开源图像编辑程序 GIMP for Mac,可以从www.gimp.org下载。但是,您可以使用任何图像编辑程序来创建 sprite 工作表。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 5-2 。机器人角色【mogura.png】精灵表

在这个 sprite 工作表中,六帧 48x48 像素的图像从左到右排列在一起(每个洞的图像构成一帧),然后在我们用Sprite(48,48)创建 sprite 时使用。这与该图像中一帧的大小(48x48 像素)相一致。在第一帧中,机器人的脸还没有出现,在接下来的四帧动画中,他慢慢地出现,直到最后一帧显示他被重击。

如果你正在使用 GIMP,你可以做以下事情来创建一个非常简单的 sprite 工作表,如图 5-2 所示:

  1. 单独下载或绘制 sprite 工作表的组件。
  2. 创建一帧大小的新图像。
  3. 将 sprite 工作表的组件粘贴到图像中。
  4. 将图像另存为 PNG 格式的单独文件,以保持透明度。
  5. 编辑图像以创建下一帧,并将其另存为新图像。
  6. 重复此过程,直到您创建了所有需要的框架。
  7. 使用 GIMP 附带的融合图层脚本合并所有图像。你可以在http://imagine.kicbak.com/blog/?p=114找到使用融合层脚本的说明。
  8. 将图像保存为新的 PNG 文件。

每帧之间的差异很重要。如果你的帧完全不同,那么动画会看起来像机器人或者不存在。在我们的 Droid 游戏中,如果一帧中有一个空洞,而下一帧中的 Droid 完全在洞外,那么当游戏运行并在两帧之间快速切换时,将不会有动画。机器人会突然出现。然而,如果有 30 帧,Droid 每次向上移动很小的量,这将不会有效地使用您的文件,并且需要更多的工作来编程。这里没有你应该制作的具体帧数。最好使用它来找出什么看起来最好,同时不要使用太多的框架。

给游戏编程

一旦你决定了游戏的规则和主题,就该进入编程阶段了。当你到了这一步,开始编程最简单的方法就是尽可能地创建你的游戏的最简单的版本。这个版本不一定需要是工作版,也绝对不需要具备前瞻游戏的所有特性。

创建最简单的版本

打地鼠游戏最简单的版本是一个显示一个洞的程序。因为最终会有不止一个洞,所以我们应该首先为洞创建一个类。执行以下操作来创建该类:

  1. http://code.9leap.net/codes/show/28286处分叉空白项目模板。这个模板不包含游戏代码,但是包含了你需要的精灵图片。

  2. Initialize the enchant library and define a class for holes by typing in the code in Listing 5-1 into your project. This will run all the code inside it whenever a Pit object (hole) is created.

    清单 5-1。 定义坑类

    enchant();
    
    Pit = Class.create(Sprite,{
         initialize:function(x,y){
              //Call the Sprite class (super class) constructor
              enchant.Sprite.call(this,48,48);
              this.image = game.assets[’mogura.png’];
              this.x = x;
              this.y = y;
         }
    });
    
  3. Create a single hole on the screen by adding the code in Listing 5-2 under the code you just entered.

    清单 5-2。 在屏幕上创建一个孔

    window.onload = function(){
         game = new Game(320, 320);
         //Load Droid image
         game.preload(’mogura.png’);
         game.onload = function(){
              var pit = new Pit(100,100);
              game.rootScene.addChild(pit);
        }
        game.start();
    }
    
  4. Click Run. A hole is displayed, as shown in Figure 5-3. The Pit class we created extends the enchant.js Sprite class, and therefore can be used in the same way as Sprite. If you encounter problems, you can see the result of this step on code.9leap.net (http://code.9leap.net/codes/show/23728).

    外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

    图 5-3 。简单打地鼠程序的结果

让机器人出现

下一步是修改我们的Pit类,使 Droid 出现。代码中唯一改变的部分是Pit类,所以我们将只展示这一部分。执行以下操作来更改该类:

  1. Modify the Pit class to change to the next frame of the Droid if the current frame is one of the first four by modifying the Pit class to match Listing 5-3. This loops the animation of the Droid appearing over and over again, but it is the groundwork for the next few steps. In this and all future code listings in the chapter, changes from the previous code are in boldface.

    清单 5-3。 更新了坑类

    Pit = Class.create(Sprite,{
         initialize:function(x,y){
              //Call the Sprite class (super class) constructor
              enchant.Sprite.call(this,48,48);
              this.image = game.assets[’mogura.png’];
              this.x = x;
              this.y = y;
              //Defines an event listener to run every frame
              this.addEventListener(’enterframe’,this.tick);
         },
         tick:function(){
              this.frame++;
              //Loop the animation once complete
              if(this.frame>=4)this.frame=0;
         }
    });
    
  2. Click Run. The Droid repeatedly appears out of the hole, as shown in Figure 5-4. If you encounter problems, you can check the result of this step on code.9leap.net (http://code.9leap.net/codes/edit/23729).

    外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

    图 5-4 。打地鼠更新了动画

调整动画速度

我们制作了一个动画,展示我们的小机器人从他的洞里快速出现。然而,他移动得太快了以至于不能击中他,所以我们需要让他慢一点。目前,在机器人精灵的第四帧之后,我们立即返回到第 0 帧。这突然回到他的洞感觉不自然。让我们重写它,这样一旦我们进行到第四帧,我们就返回到 0。执行以下操作进行这些更改:

  1. Add a variable mode to the Pit class. Use it to define how the Droid should behave with respect to animation by modifying the Pit class to match Listing 5-4.

    清单 5-4。 制作坑类动画

    Pit = Class.create(Sprite,{
         initialize:function(x,y){
              //Call the Sprite class (super class) constructor
              enchant.Sprite.call(this,48,48);
              this.image = game.assets[’mogura.png’];
              this.x = x;
              this.y = y;
              //Defines an event listener to run every frame
              this.addEventListener(’enterframe’,this.tick);
              //Keeps track of the state of the Droid
              this.mode = 0;
         },
         tick:function(){
              //only change the frame every other frame
              //the return call ends the function
              if(game.frame%2!=0)return;
              switch(this.mode){
                   //Droid is appearing from the hole
                   case 0:
                        this.frame++;
                        //change mode after completely appearing
                        if(this.frame>=4) this.mode=1;
                        break;
                   //Droid is hiding in the hole
                   case 1:
                        this.frame--;
                        //change mode after completely hiding
                        if(this.frame<=0) this.mode=0;
                        break;
              }
         }
    });
    

如果模式设置为0,这将代表机器人正在出现,但尚未完全出现,那么机器人将前进通过前四帧,然后将其模式更改为1。模式1代表躲在洞里的机器人,并使机器人颠倒它出现时使用的帧的顺序。最终的结果是,机器人会反复出现,然后消失。

我们的动画也变得更干净了。我们已经更改了代码,因此基于一个if语句,帧处理每两帧发生一次,而不是每一帧,这使它变得更好更平滑。此外,我们还添加了一个名为mode的新属性,具有模式0(出现)和模式1(隐藏中),并且我们已经基于该属性执行了一个切换来执行相应的处理。如果遇到问题,可以在code.9leap.net ( http://code.9leap.net/codes/show/23730)上查看这一步的结果。

让机器人随机出现

用我们现在的代码,如果我们复制这个洞,所有的机器人会同时出现。我们需要让机器人出现的时间随机。目前我们只有模式0(显示)和模式1(隐藏),所以我们需要添加一个模式,在两者之间等待一段随机的时间,方法如下:

  1. Upgrade the Pit class to cause random time to elapse between appearing and disappearing by replaying the Pit class with the code shown in Listing 5-5.

    清单 5-5。 升级坑类

    //function to generate random numbers
     rand = function(n){
         return Math.floor(Math.random()*n);
    }
    
    //Define a class for holes
    Pit = Class.create(Sprite,{
         initialize:function(x,y){
              //Call the Sprite class (super class) constructor
              enchant.Sprite.call(this,48,48);
              this.image = game.assets[’mogura.png’];
              this.x = x;
              this.y = y;
              //Defines an event listener to run every frame
              this.addEventListener(’enterframe’,this.tick);
              //Set the Droid mode to 2 (waiting) in the beginning.
              this.mode = 2;
              //Set the next mode as 0 (appearing)
              this.nextMode = 0;
              //wait for a random number (0-99) of frames
              this.waitFor =  game.frame+rand(100);
         },
         tick:function(){
              //only change the frame every other frame
              //the return call ends the function
              if(game.frame%2!=0)return;
              switch(this.mode){
                   //Droid is appearing from the hole
                   case 0:
                        this.frame++;
                        if(this.frame>=4) {
                        //switch to Mode 2 (waiting) after appearing
                            this.mode=2;
                        //the mode to go to after Mode 2 is Mode 1 (hide)
                        this.nextMode=1;
                        //Set a random waiting time for 0 ∼ 99 frames
                        this.waitFor = game.frame+rand(100);
                                    }
                        break;
                   //Droid is going to hide in the hole
                   case 1:
                        this.frame--;
                        //if Droid is hidden...
                        if(this.frame<=0){
                             //Switch to Mode 2 (waiting)
                             this.mode=2;
                             //The next mode should be Mode 0 (appear)
                             this.nextMode=0;
                             //Set a random waiting time for 0 ∼ 99 frames
                             this.waitFor = game.frame+rand(100);
                        }
                        break;
                   //Droid is waiting
                   case 2:
                        //if the game’s current frame is greater than
                        //the set frame to wait for...
                        if(game.frame>this.waitFor){
                             //Make a transition to the next mode
                             this.mode = this.nextMode;
                        }
                        break;
              }
         }
    });
    

我们增加了一个名为rand()的函数来生成随机数,还创建了一个新的mode,模式2(等待)。

游戏开始后经过的帧数存储在game.frame中。当waitFor中设置的帧数过去后,我们转换到下一个模式。通过在模式0(出现)和模式1(隐藏)之间插入这个,机器人出现和消失之间的时间可以被随机化。

如果有任何问题,请检查http://code.9leap.net/codes/show/23731处的代码。

实现机器人重击

到目前为止,我们已经将我们的机器人朋友设置为随机出现和消失。现在让我们创建当玩家点击他时的处理。对于所谓的“疲惫的”事件,我们使用一个touchstart事件监听器(事件监听器在第二章中介绍)。执行以下操作来实现它:

  1. Modify the Pit class by adding the bold sections of code shown in Listing 5-6.

    清单 5-6。 实现“疲惫不堪”的状态

    //Define a class for holes
    Pit = Class.create(Sprite,{
         initialize:function(x,y){
              //Call the Sprite class (super class) constructor
              enchant.Sprite.call(this,48,48);
              this.image = game.assets[’mogura.png’];
              this.x = x;
              this.y = y;
              //Defines an event listener to run every frame
              this.addEventListener(’enterframe’,this.tick);
              //Defines an event listener for when the Droid gets whacked
              this.addEventListener(’touchstart’,this.hit);
              //Set the Droid mode to 2 (waiting) in the beginning.
              this.mode = 2;
              //Set the next mode as 0 (appearing)
              this.nextMode = 0;
              //wait for a random number (0-99) of frames
              this.waitFor =  game.frame+rand(100);
         },
         tick:function(){
              //only change the frame every other frame
              //the return call ends the function
              if(game.frame%2!=0)return;
              switch(this.mode){
                   //Droid is appearing from the hole
                   case 0:
                        this.frame++;
                        if(this.frame>=4) {
                        //switch to Mode 2 (waiting) after appearing
                            this.mode=2;
                        //the mode to go to after Mode 2 is Mode 1 (hide)
                        this.nextMode=1;
                        //Set a random waiting time for 0 ∼ 99 frames
                        this.waitFor = game.frame+rand(100);
                                    }
                        break;
                   //Droid is going to hide in the hole
                   case 1:
                        this.frame--;
                        //if Droid is hidden...
                        if(this.frame<=0){
                             //Switch to Mode 2 (waiting)
                             this.mode=2;
                             //The next mode should be Mode 0 (appear)
                             this.nextMode=0;
                             //Set a random waiting time for 0 ∼ 99 frames
                             this.waitFor = game.frame+rand(100);
                        }
                        break;
                   //Droid is waiting
                   case 2:
                        //if the game’s current frame is greater than
                        //the set frame to wait for...
                        if(game.frame>this.waitFor){
                             //Make a transition to the next mode
                             this.mode = this.nextMode;
                        }
                        break;
              }
         },
         //Whack Droid
         hit:function(){
              //only when Droid has appeared at least half-way
              if(this.frame>=2){
                   //Droid after being whacked
                   this.frame=5;
                   //Switch to waiting mode
                   this.mode=2;
                   this.nextMode=1;
                   //Number of frames to wait is fixed at 10
                   this.waitFor = game.frame+10;
              }
         }
    });
    

请注意我们刚刚添加的新功能hit。如果你在机器人至少半张脸出现时点击它,重击动画将会播放。

疲惫不堪的 Droid 帧显示了十帧后,Droid 再次转换到模式1(隐藏)。以这种方式实现其他功能时切换状态的程序被称为状态机

如果您在自己的代码中遇到意外行为,您可以在http://code.9leap.net/codes/show/23739检查这一步的结果。

我们现在已经创造了打地鼠游戏的基本要素,在这个游戏中,你要对抗我们的机器人朋友。然而,我们仍然有一个问题。如果你持续不断地击打机器人,他的脸就会卡在疲劳状态。

防止机器人连续重击

我们需要确保一旦机器人被击垮,我们不能再打他,让他保持被击垮的状态。为此,让我们通过执行以下操作向我们的Pit类添加一个新属性:

  1. Update the Pit class by adding the bold sections from Listing 5-7.

    清单 5-7。 防打圈动画

    //Define a class for holes
    Pit = Class.create(Sprite,{
         initialize:function(x,y){
              //Call the Sprite class (super class) constructor
              enchant.Sprite.call(this,48,48);
              this.image = game.assets[’mogura.png’];
              this.x = x;
              this.y = y;
              //Defines an event listener to run every frame
              this.addEventListener(’enterframe’,this.tick);
              //Defines an event listener for when the Droid gets whacked
              this.addEventListener(’touchstart’,this.hit);
              //Set the Droid mode to 2 (waiting) in the beginning.
              this.mode = 2;
              //Set the next mode as 0 (appearing)
              this.nextMode = 0;
              //wait for a random number (0-99) of frames
              this.waitFor =  game.frame+rand(100);
              //stores info on whether or not the Droid
              //has already been whacked
              this.currentlyWhacked = false;
         },
         tick:function(){
              //only change the frame every other frame
              //the return call ends the function
              if(game.frame%2!=0)return;
              switch(this.mode){
                   //Droid is appearing from the hole
                   case 0:
                        this.frame++;
                        if(this.frame>=4) {
                        //switch to Mode 2 (waiting) after appearing
                        this.mode=2;
                        //the mode to go to after Mode 2 is Mode 1 (hide)
                        this.nextMode=1;
                        //Set a random waiting time for 0 ∼ 99 frames
                        this.waitFor = game.frame+rand(100);
                             }
                        break;
                   //Droid is going to hide in the hole
                   case 1:
                        this.frame--;
                        //if Droid is hidden...
                        if(this.frame<=0){
                             //Switch to Mode 2 (waiting)
                             this.mode=2;
                             //The next mode should be Mode 0 (appear)
                             this.nextMode=0;
                             //Set a random waiting time for 0 ∼ 99 frames
                             this.waitFor = game.frame+rand(100);
                             //reset flag as the whacked Droid disappears
                             this.currentlyWhacked = false;
                        }
                        break;
                   //Droid is waiting
                   case 2:
                        //if the game’s current frame is greater than
                        //the set frame to wait for...
                        if(game.frame>this.waitFor){
                             //Make a transition to the next mode
                             this.mode = this.nextMode;
                        }
                        break;
              }
         },
         //Whack Droid
         hit:function(){
              //Do nothing if the Droid has already been whacked
              if(this.currentlyWhacked)return;
              //only when Droid has appeared at least half-way
              if(this.frame>=2){
                   //Set the flag so we know he’s been whacked
                   this.currentlyWhacked = true;
                   //Droid after being whacked
                   this.frame=5;
                   //Switch to waiting mode
                   this.mode=2;
                   this.nextMode=1;
                   //Number of frames to wait is fixed at 10
                   this.waitFor = game.frame+10;
              }
         }
    });
    

在这里,我们添加了currentlyWhacked属性,这是一个标志,表示机器人是否被攻击。当 Droid 被创建时,这个属性被设置为false。每当一个机器人被重击,这个属性被设置为true,并开始隐藏动画序列。机器人消失后,这个标志被设置为false,因为一个新的机器人将从洞里出来。这样,我们就避免了机器人被连续攻击的情况。

如果您在编写与这个示例匹配的代码时遇到问题,您可以在http://code.9leap.net/codes/show/23740检查代码。

复制孔

我们几乎已经完成了只有一个洞的重击机器人游戏的基础。你可能会想,“为什么我们要大费周章只挖一个洞?”通过像我们所做的那样创建一个类,我们可以在眨眼之间完成我们的游戏。执行以下操作复制孔:

  1. Modify the game.onload function by editing it to match Listing 5-8.

    清单 5-8。 复制机器人孔

    game.onload = function(){
              //Line up holes in a 4x4 matrix
              for(var y=0;y<4;y++){
                   for(var x=0;x<4;x++){
                        var pit = new Pit(x*48+20,y*48+20);
                        game.rootScene.addChild(pit);
                   }
              }
    }
    
  2. Click Run. A line of Droids appears, as shown in Figure 5-5.

    外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

    图 5-5 。创建 Pit 类的多个实例

就像这样,我们有一个打地鼠游戏!如您所见,使用类可以节省大量时间。

因为我们将在接下来的步骤中进一步完善我们的游戏,请确保到目前为止您所做的一切都已成功完成。到目前为止的代码可以从http://code.9leap.net/codes/show/23741开始检查和分叉。

随机排列孔洞

开发游戏时,尝试不同的方法会很有帮助。例如,为了检查它将如何影响游戏,您可能想尝试让我们的重击机器人游戏中的洞随机出现在屏幕上,而不是排成 4x4 的网格。你可以在清单 5-9 和图 5-6 中看到实现这一点的变化。

清单 5-9。 随意放置孔洞

game.onload = function(){
     for(var i=0;i<7;i++){
          //Place pits randomly
          var pit = new Pit(rand(300),rand(300));
          game.rootScene.addChild(pit);
     }
}

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 5-6 。随机放置孔

因为洞的位置是随机的,所以每次你重新载入游戏时它们都会改变。不管这些孔在哪里结束,它们的工作方式完全相同,因为它们是由Pit类定义的。

现在是有趣的部分了。根据球洞的最终位置,比赛的本质将会改变。发挥你的创造力,尝试创造自己的原创球洞布置。你现在已经完成了你的第一个游戏原型!

播放、重复、完成

一旦你有了一个工作原型,下一步就是玩这个原型,并注意游戏中任何需要改进或添加的部分。

如果你玩几次我们完成的“重击机器人”游戏,你会发现少了两样重要的东西:一个分数和机器人出现次数的限制。

如果你试着让游戏的球洞变得随机,对于下面的例子,重置你的game.onload函数回到清单 5-8 中显示的。

显示分数

让我们从显示分数开始:

  1. Create the ScoreLabel class by adding the code shown in Listing 5-10 above the line of code starting with window.onload.

    清单 5-10。 ScoreLabel Class

    //ScoreLabel class definition, extending Label class
    ScoreLabel = Class.create(Label,{
         initialize:function(x,y){
              //Call the Label class constructor
              enchant.Label.call(this,"SCORE:0");
              this.x=x;
              this.y=y;
              this.score = 0;
         },
         //Adds points to the score
         add:function(pts){
              this.score+=pts;
              //Change the displayed score
              this.text="SCORE:"+this.score;
         }
    });
    
  2. Create a new instance of the ScoreLabel class by adding a new instance of the ScoreLabel class, as shown in Listing 5-11.

    清单 5-11。 创建 ScoreLabel

    game.onload = function(){
    
         //Display ScoreLabel
         scoreLabel=new ScoreLabel(5,5);
         game.rootScene.addChild(scoreLabel);
    
         //Line up holes in a 4x4 matrix
         for(var y=0;y<4;y++){
              for(var x=0;x<4;x++){
                   var pit = new Pit(x*48+20,y*48+20);
                   game.rootScene.addChild(pit);
              }
         }
    }
    
  3. Add code to increase the score whenever a Droid gets hit by adding the bold sections from Listing 5-12 to the hit function.

    清单 5-12。 增加命中分数

    //Whack Droid
    hit:function(){
         //Do nothing if the Droid has already been whacked
         if(this.currentlyWhacked)return;
         //only when Droid has appeared at least half-way
         if(this.frame>=2){
              //Set the flag so we know he’s been whacked
              this.currentlyWhacked = true;
              //Droid after being whacked
              this.frame=5;
              //Switch to waiting mode
              this.mode=2;
              this.nextMode=1;
              //Number of frames to wait is fixed at 10
              this.waitFor = game.frame+10;
              //Add score
              scoreLabel.add(1);
         }
    }
    
  4. Click Run. A score appears in the upper-left side of the screen and increases by one whenever a player whacks a Droid.

    如果您在添加这段代码时遇到问题,请查看位于http://code.9leap.net/codes/show/23777的示例代码。

限制机器人的出现

既然我们已经增加了一个分数,事情看起来就要结束了。然而,我们的机器人出现得太频繁了,以至于只要快速击中同一个点就可以获得高分。有取之不尽的机器人供应,剥夺了玩家富有挑战性的游戏体验。

让我们改变游戏,让我们的机器人只出现 30 次。以这种方式改变会迫使玩家在机器人出现的 30 次中获得尽可能多的命中。为此,请执行以下操作:

  1. Create a variable for the total number of Droids and the maximum number of appearances for a Droid, and then implement them by copying in the bold sections from Listing 5-13.

    清单 5-13。 限制机器人外观

    enchant();
    //function to generate random numbers
    rand = function(n){
         return Math.floor(Math.random()*n);
    }
    
    //Number of appearances of the Droid
    
    maxDroid = 30;
    
    //Total number of Droids
    
    totalDroid = 16;
    
    //Define a class for holes
    Pit = Class.create(Sprite,{
         initialize:function(x,y){
              //Call the Sprite class (super class) constructor
              enchant.Sprite.call(this,48,48);
              this.image = game.assets[’mogura.png’];
              this.x = x;
              this.y = y;
              //Defines an event listener to run every frame
              this.addEventListener(’enterframe’,this.tick);
              //Defines an event listener for when the Droid gets whacked
              this.addEventListener(’touchstart’,this.hit);
              //Set the Droid mode to 2 (waiting) in the beginning.
              this.mode = 2;
              //Set the next mode as 0 (appearing)
              this.nextMode = 0;
              //wait for a random number (0-99) of frames
              this.waitFor =  game.frame+rand(100);
              //stores info on whether or not the Droid
              //has already been whacked
              this.currentlyWhacked = false;
         },
         tick:function(){
              //only change the frame every other frame
              //the return call ends the function
              if(game.frame%2!=0)return;
              switch(this.mode){
                   //Droid is appearing from the hole
                   case 0:
                        this.frame++;
                        if(this.frame>=4) {
                        //switch to Mode 2 (waiting) after appearing
                        this.mode=2;
                        //the mode to go to after Mode 2 is Mode 1 (hide)
                        this.nextMode=1;
                        //Set a random waiting time for 0 ∼ 99 frames
                        this.waitFor = game.frame+rand(100);
                             }
                        break;
                   //Droid is going to hide in the hole
                   case 1:
                        this.frame--;
                        //if Droid is hidden...
                        if(this.frame<=0){
                             //Switch to Mode 2 (waiting)
                             this.mode=2;
                             //The next mode should be Mode 0 (appear)
                             this.nextMode=0;
                             //Set a random waiting time for 0 ∼ 99 frames
                             this.waitFor = game.frame+rand(100);
                             //reset flag as the whacked Droid disappears
                             this.currentlyWhacked = false;
    
                             //Reduce maximum amount of Droids
                             maxDroid--;
                             //If the amount is exceeded the Droid should not appear
                            if(maxDroid<=0) {
                                this.mode=3;
                                if(maxDroid <= -1*totalDroid + 1) {
                                    game.end(scoreLabel.score, scoreLabel.text);
                                }
                            }
                        }
                        break;
                   //Droid is waiting
                   case 2:
                        //if the game’s current frame is greater than
                        //the set frame to wait for...
                        if(game.frame>this.waitFor){
                             //Make a transition to the next mode
                             this.mode = this.nextMode;
                        }
                        break;
              }
         },
         //Whack Droid
         hit:function(){
              //Do nothing if the Droid has already been whacked
              if(this.currentlyWhacked)return;
              //only when Droid has appeared at least half-way
              if(this.frame>=2){
                   //Set the flag so we know he’s been whacked
                   this.currentlyWhacked = true;
                   //Droid after being whacked
                   this.frame=5;
                   //Switch to waiting mode
                   this.mode=2;
                   this.nextMode=1;
                   //Number of frames to wait is fixed at 10
                   this.waitFor = game.frame+10;
                   //Add score
                   scoreLabel.add(1);
              }
         }
    });
    
    //ScoreLabel class definition, extending Label class
    ScoreLabel = Class.create(Label,{
         initialize:function(x,y){
              //Call the Label class constructor
              enchant.Label.call(this,"SCORE:0");
              this.x=x;
              this.y=y;
              this.score = 0;
         },
         //Adds points to the score
         add:function(pts){
              this.score+=pts;
              //Change the displayed score
              this.text="SCORE:"+this.score;
         }
    });
    
    //Initialization
    window.onload = function(){
         game = new Game(320, 320);
         //Load Droid image
         game.preload(’mogura.png’);
         game.onload = function(){
    
              //Display ScoreLabel
              scoreLabel=new ScoreLabel(5,5);
              game.rootScene.addChild(scoreLabel);
    
              //Line up holes in a 4x4 matrix
              for(var y=0;y<4;y++){
                   for(var x=0;x<4;x++){
                        var pit = new Pit(x*48+20,y*48+20);
                        game.rootScene.addChild(pit);
                   }
              }
         }
    
         game.start();
    };
    

你可以在code.9leap.net ( http://code.9leap.net/codes/show/23778)找到这个改动的源代码。

目前,我们的重击机器人游戏结束了。然而,有很多其他的方法来制作打地鼠类型的游戏。你可以重击移动的鼹鼠或者增加等级,在等级中鼹鼠的速度和洞的位置是变化的,当然你也可以和机器人以外的其他东西战斗或者让他们从洞以外的地方出现(比如门)。通过改变规则和主题,无限的变化是可能的。

现在轮到你了。释放你的创造力,尝试创造一件属于你自己的杰作。完成游戏后,尝试将其上传到http://9leap.net。如果遇到问题,请参见第一章了解关于上传游戏的信息。

结论

在这一章中,我们重点介绍了使用 enchant.js 设计游戏的过程。我们看了游戏设计的牢不可破的规则,回顾了游戏开发过程,甚至设计了一个完全可用的打地鼠游戏。在下一章中,我们将研究游戏中最经典的原型之一,街机射击游戏,并一步一步地解释如何用 enchant.js 创建一个这样的游戏。

六、创建街机射击游戏

在第五章中,我们研究了游戏设计并创造了我们自己的打地鼠(或者我应该说是打地鼠机器人?)游戏。在这一章中,我们来看另一个游戏,一个比打地鼠更复杂的街机射击游戏。

在街机射击游戏中,我们包括几个有助于有趣游戏的功能:背景音乐,不可玩的角色(如好人和坏人),爆炸,滚动背景,等等。将所有这些元素整合在一起需要在游戏设计方面进行仔细的规划,因为我们创建的所有类都必须相互交互。例如,从玩家的船上发射的子弹应该会让敌人消失。除了介绍街机射击游戏是如何设计和编码的,我们还介绍了几个新的主题,包括指示器、关卡和旋转。

汇总列表

  1. 探索游戏和设置
  2. 为游戏奠定基础
  3. 创建玩家的船
  4. 创建拍摄类
  5. 创建 PlayerShoot 类并使船射击
  6. 制造敌人
  7. 创建敌人射击类并让敌人射击
  8. 让敌人呈弧形移动
  9. 爆炸
  10. 添加滚动背景
  11. 添加生活量表

建造街机射击游戏

让我们从我们能制作的最简单的街机射击游戏开始,然后逐步增加复杂度。本节我们编写的游戏是一个简单的射击游戏。当玩家触摸屏幕时,飞船会移动到触摸的位置,只要用户触摸它,飞船就会发射子弹穿过屏幕。敌人的船出现在屏幕的对面,向玩家的船射击。如果玩家的船被击中,游戏结束。玩家每消灭一艘敌舰,分数就会增加。游戏的目标是在被击中之前获得尽可能高的分数。

在第五章中,我们的方法是写一小段简单的代码,然后在开发过程中不断改进,在每一次迭代中玩游戏。我们在这里也是这样做的,但是因为街机射击游戏比打地鼠游戏更复杂,所以有更长的步骤。

探索游戏,设置

如果你在我们开始编码之前玩了我们将要编码的游戏,你会对游戏的工作原理有更好的了解。执行以下操作来熟悉游戏,并设置您将在其中编码的环境:

  1. http://9leap.net/games/1034玩几次游戏,感受一下我们将要制作的游戏。特别注意敌人是如何随机出现的,当屏幕上有很多敌人子弹时,游戏会变得多么困难。这款游戏因其重玩价值而在9leap.net 上广受欢迎。由于随机产生的敌人,每次的体验都不一样。
    * 在http://code.9leap.net/codes/show/29839叉模板。该模板包含您需要的必要图像文件。

`为游戏打基础

和往常一样,最好先从一个游戏最简单的元素开始,然后再加入更复杂的元素。在这里,我们设置了Core对象,设置了背景,并创建了一个乐谱。执行以下操作进行设置:

  1. Initialize the enchant.js library and create the Core entity by copying the code in Listing 6-1 into the blank template. Because of the complexity of this section, we label sections of code with comments so we can refer to them later in the chapter.

    清单 6-1。 街机射手的基础

    enchant();
    //Class Definitions
    
    window.onload = function() {
        game = new Core(320, 320);
        //Game Properties
        game.fps = 24;
    
        game.onload = function() {
        };
    
        game.start();
    };
    
  2. Create a black background by typing the code in Listing 6-2 into the game.onload function. This specifies the backgroundColor of rootScene to be black.

    清单 6-2。 创建黑色背景

    //In-Game Variables and Properties
    game.rootScene.backgroundColor = 'black';
    

    rootScenebackgroundColor属性可以接受所有标准颜色的名称和颜色的十六进制值(例如,#FF0000代表红色)。

  3. 单击运行。屏幕应该会变黑。

  4. Create a game variable for the player’s score by adding the code in Listing 6-3 to the //Game Properties section. Later, we’ll make this variable increase in value whenever enemies are hit by bullets from the player’s ship.

    清单 6-3。 创建分数变量

    game.score = 0;
    
  5. Create a ScoreLabel and add it to rootScene by adding the code in Listing 6-4 to the //In-game Variables and Properties section. The (8,8) specifies the top-left corner of the label to be placed 8 pixels to the right and 8 pixels down from the top-left corner of the game. Later, we’ll update the value of the ScoreLabel with the value of game.score every frame.

    清单 6-4。 为 rootScene 创建并添加分数标签

    scoreLabel = new ScoreLabel(8, 8);
    game.rootScene.addChild(scoreLabel);
    

    ScoreLabel类是名为ui.enchant.js的插件的一部分,该插件包含在enchantjs.com的下载包中。如果你早点从 code.9leap.net 接手这个项目的话,它也包括在内。如果没有,在继续之前,您需要确保行<script src='/static/enchant.js-latest/plugins/ui.enchant.js'></script>被添加到您的index.html文件中。

  6. 单击运行。ScoreLabel应出现在屏幕顶部,并带有单词“SCORE:”。

如果您的代码遇到任何问题,您可以在http://code.9leap.net/codes/show/29841找到一个完整的工作示例。

创建玩家的船

下一步是创建玩家的船,我们将创建一个类。执行以下操作来创建它:

  1. Create a class definition for the player by entering the code in Listing 6-5 to the //Class Definitions section. For a refresher, the enchant.Sprite declaration creates the new class as an extension of the Sprite class, which means all properties and methods of the Sprite class will work on the Player class as well. Everything in the initialize function will be run when a Player object is created.

    清单 6-5。 玩家类

    // Player class
    var Player = enchant.Class.create(enchant.Sprite, {
        initialize: function(x, y){
    
        }
    });
    
  2. Preload graphic.png, which contains all the images used in this game, by adding the code in Listing 6-6 to the //Game Properties section. Figure 6-1 shows the graphic.

    清单 6-6。 预加载图像

    game.preload('graphic.png');
    

    外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

    图 6-1 。Graphic.png

  3. Define the Player class as a 16x16 instance of the Sprite class and specify graphic.png as its image by entering the code in Listing 6-7 into the initialize function of the Player class.

    清单 6-7。 指定尺寸和图像

    enchant.Sprite.call(this, 16, 16);
    this.image = game.assets['graphic.png'];
    
  4. Make the location of Player to be whatever was specified when it was created and set the frame by adding the code in Listing 6-8 directly under what you just added.

    清单 6-8。 设置位置和帧

    this.x = x;
    this.y = y;
    this.frame = 0;
    
  5. Create a variable to keep track of when the screen is being touched by adding the code in Listing 6-9 to //Game Properties. This will set in the touch event listeners we’ll create next, and will be used to determine if bullets should be fired from the ship later.

    清单 6-9。??【跟踪触摸】

    game.touched = false;
    
  6. Back in the initialize function, add an event listener to move the Player to Y position of the touch event when a touchstart event occurs by entering the code in Listing 6-10. When the touchstart event occurs, we also set the game.touched variable to true. Notice how we don’t need to put a line break between player.y = e.y; and game.touched = true;. The semicolons delineate the commands.

    清单 6-10。 添加 Touchstart 事件监听器

    game.rootScene.addEventListener('touchstart',
            function(e){ player.y = e.y; game.touched = true; });
    
  7. Below that, add event listeners for both touchend and touchmove events to take care of all possible interaction from the player by adding the code in Listing 6-11.

    清单 6-11。 附加事件监听器

    game.rootScene.addEventListener('touchend',
            function(e){ player.y = e.y; game.touched = false; });
    game.rootScene.addEventListener('touchmove',
            function(e){ player.y = e.y; });
    
  8. Create an instance of the Player class and add it to rootScene by entering the code in Listing 6-12 directly below what you just added, still inside the initialize function.

    清单 6-12。Player添加到rootScene

    game.rootScene.addChild(this);
    
  9. Create an instance of Player by adding the code in Listing 6-13 to the //In-game Variables and Properties section. The instance of Player is automatically added to rootScene because of the last step.

    清单 6-13。 创建Player 的实例

    player = new Player(0, 152);
    
  10. Click Run. The ship appears on the screen. If you click and hold, the ship will follow your cursor up and down on the screen.

如果您在本节中遇到问题,可以在`http://code.9leap.net/codes/show/29845`找到一个工作代码示例。

创建拍摄类

这个游戏中会出现两种弹药:玩家船射的弹药和敌人射的弹药。这两种类型使用相同的图像,所以我们将从创建一个通用的Shoot类开始。执行以下操作来创建它:

  1. Create the basic Shoot class by entering the code in Listing 6-14 beneath the //Player Class definition. It should have two functions: initialize and remove. It should also extend the Sprite class. When an instance of the Shoot class is created, we will pass three values to it: an X coordinate, a Y coordinate, and a direction.

    ***清单 6-14。***创建射击类

    // Shoot class
    var Shoot = enchant.Class.create(enchant.Sprite, {
        initialize: function(x, y, direction){
        },
        remove: function(){
        }
    });
    
  2. Create the Shoot class as a 16x16 instance of the Sprite class, and create and assign necessary variables by entering the code in Listing 6-15 into the initialize function of the Shoot class. We will use the moveSpeed variable next to control the speed of movement and allow for easy modification of ammunition speed later on.

    清单 6-15。Shoot类的实例变量

    enchant.Sprite.call(this, 16, 16);
    this.image = game.assets['graphic.png'];
    this.x = x;
    this.y = y;
    this.frame = 1;
    this.direction = direction;
    this.moveSpeed = 10;
    
  3. Create an enterframe event listener to control movement by entering the code in Listing 6-16 directly beneath this.movespeed = 10;. Code entered here will be run on instances of Shoot every frame.

    清单 6-16。 创建一个enterframe事件监听器

    this.addEventListener('enterframe', function(){
            });
    

我们将在下一节中向这个事件监听器添加代码。

用 Cos 和 Sin 控制方向

我们创建了Shoot类来接受方向,但是我们还不知道传递什么样的值来指示方向。我们也有一个事件监听器来处理运动,但是我们还不知道如何处理偏离方向的运动。我们如何做到这一点?具有功能cosinesine,通常简称为cossin

要理解这些功能,首先需要理解单位圆的方向,如图图 6-2 所示。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 6-2 。单位圆

单位圆用弧度表示方向。一弧度等于一个圆的半径的长度,在它的圆周上排成一行。圆圈中的pi符号是一个数学常数,等于一个圆的周长除以其直径(约 3.14)。这有什么关系?因为 cos 和 sin 只接受以弧度表示的值,而左右主方向分别等于pi0(或2 * pi)。

假设你想从圆心画一条长度为 1 的线。Cossin将分别给出该线端点的 x 和 y 坐标,根据pi给出一个方向。例如,如果我们想向点的右边移动 1,我们可以将pi传递给cos来找出我们需要沿着 x 轴移动多少才能到达那里(-1)。我们还将通过pisin来找出我们需要沿着 y 轴(0)移动多少。

代码就是这样处理运动的。我们根据pi选择一个方向,然后使用cossin来计算每一帧精灵移动多少。

  • 4.  Inside the event listener you just created, specify how instances of the Shoot class should move based off cos and sin by entering the code in Listing 6-17. Multiplying the results of the calculations by moveSpeed allows the ammunition to be manipulated in terms of speed later, if needed.

    清单 6-17。CosSin 控制移动

    this.x += this.moveSpeed * Math.cos(this.direction);
    this.y += this.moveSpeed * Math.sin(this.direction);
    
  • 5.  Designate instances of the Shoot class to call the remove function if the shots stray far outside the bounds of the game screen by entering the code in Listing 6-18 inside the enterframe event listener, below what you just added. We could use 0 for the minimum allowed values of X and Y before the remove function is called, but using –this.width and –this.height ensures the shots don’t disappear off the screen unnaturally. Do not worry about defining what the remove function does just yet.

    清单 6-18。 调用remove函数

    if(this.y > 320 || this.x > 320 || this.x < -this.width || this.y < -this.height){
                this.remove();
    }
    
  • 6.  Inside the definition of the remove function, under the definition of the initialize function, specify what should happen when the remove function is called by entering the code in Listing 6-19. The delete command removes a given instance of the Shoot class from memory. In a very long game, if this is not specified it could bog down the system.

    清单 6-19。remove功能

    game.rootScene.removeChild(this);
    delete this;
    
  • 7.  Add instances of the Shoot class to rootScene on creation by adding the code in Listing 6-20 to the initialize function, after the event listener.

    清单 6-20。Shoot添加到rootScene

    game.rootScene.addChild(this);
    

创建 PlayerShoot 类并让船射击

我们创建了通用的Shoot类,但是现在我们需要为玩家的船发射的弹药创建一个类,并在船发射时创建它的实例。执行以下操作来创建该类:

  1. In the //Class Definitions section, create the PlayerShoot class and its initialize function by entering the code in Listing 6-21.

    清单 6-21。 创建 PlayerShoot 类

    // PlayerShoot class
    var PlayerShoot = enchant.Class.create(Shoot, { // Succeeds bullet class
        initialize: function(x, y){
        }
    });
    
  2. Create the PlayerShoot class as an instance of the Shoot class, specifying 0 as the direction, by inserting the code in Listing 6-22 into the initialize function. Remember the unit circle? The value of 0 is equal to a direction facing the right side of the screen. Because the ship is on the left side of the screen, bullets fired will head toward the right.

    清单 6-22。 创建Shoot类的实例

    Shoot.call(this, x, y, 0);
    
  3. We now need make the ship fire instances of the PlayerShoot class. Go back to the Player class definition and create an enterframe event listener inside the initialize function by entering the code in Listing 6-23 right above the line game.rootScene.addChild(this);.

    清单 6-23。 Enterframe 事件监听器

    this.addEventListener('enterframe', function(){
    });
    
  4. Inside this event listener, add the code in Listing 6-24 to create an if statement to be executed once every three frames and to be executed if the game is being touched.

    清单 6-24。 If 语句控制出手

    if(game.touched && game.frame % 3 === 0){
    }
    
  5. Inside the if statement, create a new instance of the PlayerShoot class by entering the code in Listing 6-25.

    清单 6-25。 创建一个PlayerShoot实例

    var s = new PlayerShoot(this.x, this.y);
    
  6. 单击运行。当你点击屏幕时,你的船会发射弹药穿过屏幕。

如果您在本节中遇到问题,您可以在http://code.9leap.net/codes/show/29907找到一个完整的工作代码示例。

创造敌人

我们的船现在可以发射子弹,但现在我们需要为船创造一些可以射击的东西。执行以下操作为敌人创建一个职业并将他们添加到游戏中:

  1. Create the basic Enemy class definition with an initialize and remove function by adding the code in Listing 6-26 to the //Class Definitions section, below the Player class definition.

    清单 6-26。 基础Enemy

    //Enemy class
    var Enemy = enchant.Class.create(enchant.Sprite, {
        initialize: function(x, y){
        },
        remove: function(){
        }
    });
    
  2. Make the Enemy class a 16x16 instance of the Sprite class and assign the frame, x, and y variables by entering Listing 6-27 into the initialize function.

    清单 6-27。 制作 Sprite 实例并赋值变量

    enchant.Sprite.call(this, 16, 16);
    this.image = game.assets['graphic.png'];
    this.x = x;
    this.y = y;
    this.frame = 3;
    
  3. Specify the direction of movement for enemies to be to the left in terms of the unit circle (Math.PI), and create a variable for movement speed by entering the code in Listing 6-28 below what you just entered.

    清单 6-28。 为方向和运动创建变量

    this.direction = 0;
    this.moveSpeed = 3;
    
  4. Under what you just entered, create an event listener to move the enemy using the variables you just created by entering the code in Listing 6-29.

    清单 6-29。 移动敌人

    // Define enemy movement
    this.addEventListener('enterframe', function(){
            this.x -= this.moveSpeed * Math.cos(this.direction);
            this.y += this.moveSpeed * Math.sin(this.direction);
    });
    
  5. Make the Enemy call the remove function (which we’ll define soon) if it is outside the dimensions of the screen by entering the code in Listing 6-30 directly under the line this.y += this.moveSpeed * Math.sin(this.direction);.

    清单 6-30。 除去屏幕外的敌人若

    // Disappear when outside of screen
    if(this.y > 320 || this.x > 320 || this.x < -this.width || this.y < -this.height){
            this.remove();
    }
    
  6. Finally, have the Enemy add itself to rootScene when it is created by entering the code in Listing 6-31 into the initialize function, under the event listener you just added.

    清单 6-31。 给 rootScene 添加敌人

    game.rootScene.addChild(this);
    
  7. Define the remove function by entering Listing 6-32 into the remove function. This will remove the enemy from rootScene and delete it from memory. Also, it will remove the enemy from an array we’re going to create to keep track of enemies. (Hence the delete enemies[this.key];, which will be explained soon.)

    清单 6-32。 清除功能

    game.rootScene.removeChild(this);
    delete enemies[this.key];
    delete this;
    
  8. In the //In-Game Variables and Properties section, create an array to keep track of enemies by entering the code in Listing 6-33.

    清单 6-33。 制造敌阵

    enemies = [];
    
  9. Under the new array, create an enterframe event listener for the game to create enemies randomly by entering the code in Listing 6-34. We create a variable inside the Enemy called enemy.key and assign the game’s current frame to it because this gives us something to keep track of the enemies with. If we do not do this, we would not be able to reference a specific enemy later on, which is needed when the enemies stray off screen or are hit with ammunition from the ship. Enemies have approximately a 1 in 10 chance of being created because of if(Math.random()*100 < 10) and, if created, are placed randomly on the y-axis.

    清单 6-34。 在游戏中制造敌人

    game.rootScene.addEventListener('enterframe', function(){
        if(Math.random()*100 < 10){
            var y = Math.random() * 320;
            var enemy = new Enemy(320, y);
            enemy.key = game.frame;
            enemies[game.frame] = enemy;
        }
    });
    
  10. Update the game’s scoreLabel every frame by entering the code in Listing 6-35 underneath the if statement, but still inside the event listener.

***清单 6-35。*** 每帧更新游戏分数

```js
scoreLabel.score = game.score;
```
  1. 点击运行。敌人被创造出来并飞过屏幕。然而,当船上的子弹击中他们时,什么也没有发生。
  2. Make the ship’s ammunition destroy enemies with an event listener by entering the code in Listing 6-36 into the PlayerShoot class definition, under Shoot.call(this, x, y, 0);. The way this for loop is constructed causes the program to go through every single member of the enemies array, checking to see if the given bullet is in contact with it. If so, both the bullet and the enemy in the array are removed, and the player’s score is increased by 100.
***清单 6-36。*** 制伏敌人

```js
this.addEventListener('enterframe', function(){
    // Judges whether or not player's bullets have hit enemy
    for(var i in enemies){
        if(enemies[i].intersect(this)){
            // Eliminates enemy if hit
            this.remove();
            enemies[i].remove();
            //Adds to score
            game.score += 100;
        }
    }
});
```
  1. 单击运行。你现在可以通过射击来消灭敌人。如果您在本节中遇到问题,可以在http://code.9leap.net/codes/show/29929找到一个工作代码示例。

创建敌人射击类并让敌人射击

在这一点上,没有办法输掉比赛,这不是很引人注目。让我们创建一个敌人弹药的职业,让敌人向飞船开火,通过以下方式增加难度:

  1. Create the enemyShoot class by adding the code in Listing 6-37 in the //Class Definitions section. Create it as an instance of the Shoot class, with the direction set as Math.PI, as that faces left in the unit circle.

    清单 6-37。 排敌班

    // Class for enemy bullets
    var EnemyShoot = enchant.Class.create(Shoot, { // Succeeds bullet class
        initialize: function(x, y){
            Shoot.call(this, x, y, Math.PI);
        }
    });
    
  2. Add an enterframe event listener inside the initialize function by adding the code in Listing 6-38 under the line that begins with Shoot.call. This event listener should contain an if statement specifying that the game should end if the center of player and the center of a given enemy bullet is 8 pixels or less at any given time.

    清单 6-38。 指定playerShootplayer 之间的点击次数

    this.addEventListener('enterframe', function(){
        if(player.within(this, 8)){
            game.end(game.score, "SCORE: " + game.score);
        }
    });
    
  3. Make the Enemy class create instances of the enemyShoot class every 10 frames by changing the if statement inside the Enemy enterframe event listener to match what is shown in Listing 6-39. The variable age can be called on any Entity and gives the number of frames the Entity has been alive.

    清单 6-39。 制敌射击

    // Disappear when outside of screen
    if(this.y > 320 || this.x > 320 || this.x < -this.width || this.y < -this.height){
        this.remove();
    }else if(this.age % 10 === 0){ // Fire every 10 frames
        var s = new EnemyShoot(this.x, this.y);
    }
    
  4. 点击运行。敌人现在开始反击。

如果您在本节中遇到问题,可以在http://code.9leap.net/codes/show/30105找到一个工作代码示例。

使敌人沿弧线移动

敌人现在沿直线移动。这使得玩游戏相当直接,但有点简单。让我们让敌人以弧线移动,让游戏更有趣。我们将通过创建一个变量来指定方向应该向上运动还是向下运动(theta ),这取决于敌人在哪里被创建,然后在每一帧稍微改变方向。为此,请执行以下操作:

  1. Each enemy needs to have a variable that will be used to change its direction. Create a variable (theta) that is passed as an argument when an Enemy is created by changing the opening line of the initialize function to match the code in Listing 6-40.

    清单 6-40。 添加了theta变量

    initialize: function(x, y, theta){
    
  2. Although direction is specified in terms of the unit circle, it’s easier to work in degrees for specific amounts other than 0 or Math.PI. We’ll be passing a value in degrees to theta, so convert it to radians by entering the code in Listing 6-41 right before the line that reads this.direction = 0;.

    清单 6-41。theta转换成弧度

    this.theta = theta * Math.PI / 180;
    
  3. Inside the enterframe event listener of the Enemy class, make theta change the direction of the Enemy every frame by entering the code in Listing 6-42 directly under the line that reads this.addEventListener('enterframe', function(){.

    清单 6-42。 递增Enemy方向

    this.direction += this.theta;
    
  4. Now enemies can accept a value for theta, and this will change the direction of the Enemy every frame, but we need to specify how the enemies are created to really use this. To accomplish this, change the if statement inside the game’s rootScene enterframe event listener to match the code in Listing 6-43. If the Enemy is created in the upper half of the screen, the enemy will arc upwards (direction angle will increase by 1 each frame). If it is created in the lower half of the screen, it will arc downwards (direction angle will decrease by 1 each frame).

    清单 6-43。 制造敌人那道弧线

    if(rand(100) < 10){
            // Make enemies appear randomly
            var y = rand(320);
            if (y < 160) {
                    theta = 1;
            } else {
                    theta = -1;
            }
            var enemy = new Enemy(320, y, theta);
            enemy.key = game.frame;
            enemies[game.frame] = enemy;
    }
    
  5. Click Run. Enemies move in an arc, making the game more compelling. Your game should appear as it does in Figure 6-3.

    外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

    图 6-3 。简单的射击游戏

如果您在本节中遇到问题,可以在http://code.9leap.net/codes/show/30564找到一个工作代码示例。

强化游戏

目前,我们的游戏在目前的状态下是一个工作游戏。我们可以把它留在这里,转到另一个游戏,但让我们研究一些方法,通过添加一些功能来使游戏更引人注目。

我们将对原来的游戏做一些补充,并在接下来的部分解释如何做。

爆炸

先来加点爆款。目前,当敌舰被击落时,它们就消失了。让我们通过在敌舰被击中时引起爆炸来增加一点刺激。

  1. Download the explosion (Figure 6-4) sprite sheet from http://enchantjs.com/assets/img/effect0.gif and add it to your project. We’ll use this sprite sheet for our explosion.

    外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

    图 6-4 。爆炸效果图

  2. Create a basic class, called Blast, as an extension of the Sprite class by adding the code in Listing 6-44 into the //Class Definitions section. The class should have an initialize and remove function.

    清单 6-44。 爆炸类

    // Class for explosions
    var Blast = enchant.Class.create(enchant.Sprite, {
        initialize: function(x, y){
        },
        remove: function(){
        }
    });
    
  3. Inside the initialize function, create the Blast class as a 16x16 instance of the Sprite class, and pass the x and y arguments to the local variables x and y by entering the code in Listing 6-45.

    清单 6-45。 在爆炸中分配变量

    enchant.Sprite.call(this,16,16);
    this.x = x;
    this.y = y;
    
  4. Jump down to the window.onload function and add a preload statement under game.preload('graphic.png'); to add the sprite sheet of the explosion by entering the code in Listing 6-46.

    清单 6-46。 预载爆炸图像

    game.preload('effect0.gif');
    
  5. Back in the initialize definition of the Blast class, under this.y = y;, add a statement to use effect0.gif as the image for the explosion by adding the code in Listing 6-47.

    清单 6-47。 指定effect0.gifBlast图像

    this.image = game.assets['effect0.gif'];
    
  6. Specify the frame to start at 0, and specify a duration of 20 frames by entering Listing 6-48 on the next line. We will use the duration soon to draw out the animation over a specific amount of time.

    清单 6-48。 指定起始帧和持续时间

    this.frame = 0;
    this.duration = 20;
    
  7. Below that, create an enterframe event listener by entering the code in Listing 6-49. We’ll use this event listener to control the frame of the explosion.

    清单 6-49。 事件监听器为Blast

    this.addEventListener('enterframe', function(){
    }
    
  8. Inside the event listener, create a statement to set the frame of the explosion to go from 0 to 4 over a period of 20 frames by entering the code in Listing 6-50. This is accomplished with an algorithm. This algorithm first takes the current number of frames the explosion has been alive for (this.age) and divides it by the desired duration of the animation (this.duration) to get a fraction representing how far through the animation sequence the explosion should be. This is multiplied by 5 because the total sequence, shown in the sprite sheet, is 5 frames long. At this point, the result most likely has a decimal value (such as 4.42), so it is rounded down with Math.floor, and the result is what is assigned to the current frame of the explosion. The result is that the explosion progresses smoothly over 20 frames, and this value can be changed simply by editing the value of duration from 20 to something else.

    清单 6-50。frame 赋值

    // Explosion animation
    this.frame = Math.floor(this.age/this.duration * 5);
    
  9. Beneath that, but still in the event listener, enter the code in Listing 6-51 to create an if statement that calls the remove function if the explosion has been alive for the desired duration. Note that if there is only one statement after the if statement, curly braces ({}) are not required.

    清单 6-51。 调用remove函数

    if(this.age == this.duration) this.remove();
    
  10. Under the if statement, outside of the event listener, but still inside the initialize function, add the blast to rootScene by entering the code in Listing 6-52.

***清单 6-52。*** 给`rootScene` 添加冲击波

```js
game.rootScene.addChild(this);
```
  1. Make the remove function remove the blast from rootScene by entering the code in Listing 6-53 into the definition of the remove function.
***清单 6-53。*** 清除根景爆炸

```js
game.rootScene.removeChild(this);
```
  1. Make playerShoot create an instance of the Blast class if it hits an enemy by rewriting the definition of playerShoot to match the code in Listing 6-54. The line you should add is in bold type.
***清单 6-54。***`playerShoot`创建`Blast` 的实例

```js
// PlayerShoot class
var PlayerShoot = enchant.Class.create(Shoot, { // Succeeds bullet class
    initialize: function(x, y){
        Shoot.call(this, x, y, 0);
        this.addEventListener('enterframe', function(){
                // Judges whether or not player's bullets have hit enemy
                for(var i in enemies){
                    if(enemies[i].intersect(this)){
                    //Start Explosion
                    var blast = new Blast(enemies[i].x,enemies[i].y);
                        // Eliminates enemy if hit
                        this.remove();
                        enemies[i].remove();
                        //Adds to score
                        game.score += 100;
                        }
                }
                });
    }
});
```
  1. 单击运行。尝试玩游戏,看看当敌人被玩家船上的子弹击中时,爆炸是如何出现的。如果你做的一切都正确,游戏应该会出现在图 6-5 中。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 6-5 。具有爆炸功能的街机射击游戏

如果您在本节中遇到问题,您可以在http://code.9leap.net/codes/show/30633找到一个完整的工作代码示例。

添加滚动背景

现在,我们已经加入了爆炸,事情开始看起来有点好,但我们仍然有一段路要走。为了增加准现实主义的元素,让我们为我们的游戏创建一个滚动背景。

首先,我们需要一个正好是屏幕两倍宽的背景图像。图像的左右两边应该完全一样,最左边或最右边都没有星星。我们一会儿就知道为什么了。

我们一次向左滚动一个像素,当我们到达终点时,我们循环回到起点,在整个游戏中不断重复这个过程。这就是为什么我们希望背景图片的左右两边是一样的。如果玩家注意到背景中的突然变化,看起来会很不自然。我们将使用图 6-6 中所示的图像。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 6-6 。png 图形

通过创建Background类并加入这个循环动作,我们创造了一个无休止滚动的背景效果。执行以下操作来创建Background类并实现它:

  • 14.  In the //Class Definitions section, create a class for the background as an extension of the Sprite class by entering the code in Listing 6-55. The only method needed is the initialize method, as we never remove it from rootScene.

    清单 6-55。 Background

    // Background class
    var Background = enchant.Class.create(enchant.Sprite, {
        initialize: function(){
        }
    });
    
  • 15.  Inside the initialize function, create Background as a 640x320 Sprite by entering in Listing 6-56. This dimension is just as tall and twice as wide as the game screen.

    清单 6-56。 创建为精灵的实例

    enchant.Sprite.call(this,640,320);
    
  • 16.转到http://enchantjs.com/?p=731并下载图像文件bg.png,用作背景。

  • 17.将文件上传到您在code.9leap.net中的项目。

  • 18.  Inside the window.onload function, under the //Game Properties section, add Listing 6-57 to preload bg.png.

    清单 6-57。 预加载背景图像

    game.preload('bg.png');
    
  • 19.  Back inside the initialize function of the Background class, add Listing 6-58 to the variable declarations to position the background and assign bg.png to be used as the background image.

    清单 6-58。 定位和图像变量

    this.x = 0;
    this.y = 0;
    this.image = game.assets['bg.png'];
    
  • 20.  Below that, but still inside the initialize function, add an enterframe event listener to move the background to the left by one pixel every frame by entering in the code in Listing 6-59.

    清单 6-59。 移动Background每一帧

    this.addEventListener('enterframe', function(){
            this.x--;
    });
    
  • 21.  Inside the event listener, underneath this.x--;, write a statement to reset the position of Background if it has been scrolled all the way over by entering the code in Listing 6-60. We know if the image has been scrolled all the way over if its x position is -320 or less.

    清单 6-60。 重置背景位置

    if(this.x<=-320) this.x=0;
    
  • 22.  Under the event listener, but still inside the initialize function, add Background to rootScene by entering the code in Listing 6-61.

    清单 6-61。 给 rootScene 添加背景

    game.rootScene.addChild(this);
    
  • 23.  Finally, in the game.onload function, replace the line that reads game.rootScene.backgroundColor = 'black'; with the code in Listing 6-62 to create an instance of Background.

    清单 6-62。 创建Background 的实例

    background = new Background();
    
  • 24.单击运行。游戏应该出现在图 6-7 中。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 6-7 。背景滚动的街机射击游戏

如果您在本节中遇到问题,可以在http://code.9leap.net/codes/show/30704找到一个工作代码示例。

添加生命量表

目前,玩家只需一击就会死亡。如果他们在至少三次击中后倒下,似乎会公平得多。考虑到这一点,让我们添加一些东西,让玩家有多条命,并允许他们在死亡前承受几次打击。为此,请执行以下操作:

  1. In the game.onloadfunction, under the //In-Game Variables and Properties section, add a variable as part of the Core object (game) to keep track of a player’s life by entering the code in Listing 6-63.

    清单 6-63。 创建一个生命变量

    game.life = 3;
    

    我们现在已经初始化了一个代表玩家剩余生命数的变量。因为我们在game.onload内部这样做,并使用game.life作为变量名(使用game.前缀使其成为这里称为gameCore对象的一部分),所以它可以在游戏内部的任何地方被引用。

  2. Rewrite the EnemyShoot class to reduce the amount of life the player has by 1 if hit by an enemy bullet by making the class match the code in Listing 6-64. You should erase the line that reads game.end(game.score, "SCORE: " + game.score); and replace it with the code in bold type.

    清单 6-64。 命中时减少生命

    // Class for enemy bullets
    var EnemyShoot = enchant.Class.create(Shoot, { // Succeeds bullet class
        initialize: function(x, y){
            Shoot.call(this, x, y, Math.PI);
            this.addEventListener('enterframe', function(){
                if(player.within(this, 8)){     // Bullet has hit player
                        game.life--;
                    }
            });
        }
    });
    
  3. Under game.life--;, but still inside the if statement, write another if statement to end the game if the player’s life is less than or equal to 0 by entering the code in Listing 6-65.

    清单 6-65。 结束游戏如果没有生命

    if(game.life<=0)
    game.end(game.score, "SCORE: " + game.score);
    

    我们已经重写了游戏,当玩家被击中时生命值减少 1,如果生命值为 0,游戏就结束了。我们可以创建一个类来显示生活,但是不需要在屏幕上添加太多内容,所以在这种情况下,简单地在game.onload中创建指示器会更有效。

    我们将使用作为ui.enchant.js插件的一部分提供的MutableText类来制作指示器。MutableText类与Label类非常相似,但是它不使用计算机中已经安装的字体在屏幕上创建文本,而是使用 sprite 表中文本字符的图像,很像Sprite

    外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 注意ScoreLabel类是MutableText类的扩展,行为类似。

    如果我们在这里使用常规的Label类,结果看起来会很便宜,因为将使用系统字体。通过使用MutableText,你可以使用更好看的字体,并且因为使用图像而不是系统字体,你可以确保字母看起来总是一样的,不管游戏运行在什么样的浏览器或操作系统上。

  4. Within the game.onload function, below the line that reads player = new Player(0, 152);, create a new instance of MutableText by entering the code in bold type from Listing 6-66. The first argument (8), specifies the X position of the upper-left corner of the new instance, the second argument (320 – 32) specifies the Y position, the third (game.width) specifies the width of the label, and the fourth specifies the text that should be shown.

    清单 6-66。 创造了lifeLabel

    game.onload = function() {
        //In-Game Variables and Properties
        background = new Background();
        game.life = 3;
    
        scoreLabel = new ScoreLabel(8, 8);
        game.rootScene.addChild(scoreLabel);
        player = new Player(0, 152);
        enemies = [];
        // Display life
        lifeLabel = new MutableText(8, 320 - 32, game.width, "");
    
  5. Beneath the line that reads lifeLabel = new MutableText(8, 320 – 32, game.width, "");, but still inside the game.onload function, create an event listener to update the text of lifeLabel with a number of 0s equal to however many lives the player has left by entering the code in Listing 6-67.

    清单 6-67。 显示生命数量

    lifeLabel.addEventListener('enterframe', function(){
        this.text = "LIFE " + "OOOOOOOOO".substring(0, game.life);
    });
    

    因为我们在一个enterframe事件侦听器中定义了lifeLabel的文本,所以生命量表将在每一帧中更新。

    我们来看看"OOOOOOOOO".substring(0, game.life);部分。这个“O”代表游戏中的单身生活。"OOOOOOOOO"是一个 JavaScript 字符串对象,所以我们可以直接调用应用于该字符串的方法。我们可以通过使用substring()方法从一个字符串的开头到指定的点提取文本。

    应该注意的是,substring()方法的第二个参数表示子字符串应该从中提取字符的点。请记住,字符串和数组中的位置引用以 0 开头。这意味着如果game.life等于 3,子串用"OOOOOOOOO".substring(0, game.life);处理,子串将在位置 0、位置 1 和位置 2 返回 0,但在到达第三个位置之前会停止。

    这也是为什么游戏以三条命开头,在游戏中用三个 0 来代表("LIFE 000")。随着生命的减少,它会显示"LIFE OO"然后是"LIFE O."

    正如你所看到的,enchant.js 允许你轻松地创建游戏指示器,而不必创建一个全新的类。

  6. Finally, under the event listener, add the lifeLabel to rootScene by entering in the code in Listing 6-68.

    清单 6-68。 给 rootScene 添加 life label

    game.rootScene.addChild(lifeLabel);
    
  7. 单击运行。寿命计将出现在图 6-8 的中。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 6-8 。带生命计的街机射击游戏

如果您在本节中遇到问题,您可以在http://code.9leap.net/codes/show/30801找到一个完整的工作代码示例。

结论

希望你能看到最终拍摄程序的总体流程与我们在本章前面看到的简单原型没有太大的不同。事实上,Background类和许多其他类与我们在本章开始时检查的原型完全相同。

正如我们看到的第五章一样,在重击机器人的例子中,通过一点一点地重写和改进原型,一个成熟而复杂的游戏可以被开发出来。

在这一章中,我们探索了射击游戏的开发,看看如何为子弹、敌人和玩家创建类,以及如何处理子弹和敌人或玩家飞船之间的碰撞检测。然后,我们研究了可以用来改进基本游戏的更高级的概念,包括爆炸、生命标尺等等。

在下一章,我们将看看如何在code.9leap.net之外创建独立游戏,并研究如何使用 3D 来创建带有gl.enchant.js插件的游戏。`

转载请注明出处或者链接地址:https://www.qianduange.cn//article/20987.html
标签
评论
发布的文章

C/C | 每日一练 (2)

2025-02-24 13:02:49

Linux性能监控工具汇总

2025-02-24 13:02:48

Python常见面试题的详解16

2025-02-24 13:02:48

QQ登录测试用例报告

2025-02-24 13:02:47

大家推荐的文章
会员中心 联系我 留言建议 回顶部
复制成功!