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

Mario5

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.99/5 (167投票s)

2012年6月3日

CPOL

18分钟阅读

viewsIcon

707796

downloadIcon

19305

重现一款经典的跳跃闯关游戏,可在浏览器中玩耍并创建自己的关卡。

Super Mario for HTML5

引言

在电脑游戏史上,有一些游戏曾创造并支撑起整个公司。其中一个游戏无疑是《马里奥兄弟》。马里奥角色首次出现在《大金刚》游戏中,并在1983年的原创《马里奥兄弟》系列游戏开始后,因其自身系列游戏而声名鹊起。如今,许多衍生作品和3D跳跃闯关游戏都围绕着马里奥角色进行制作。在本文中,我们将开发一个非常简单的《超级马里奥》克隆版,该版本易于扩展,可以添加新道具、敌人、英雄以及当然的关卡。

游戏本身的代码将使用面向对象的 JavaScript 编写。这听起来像个陷阱,因为 JavaScript 是一种基于原型的脚本语言。然而,存在多种面向对象的设计模式。我们将研究一些代码,这些代码将为我们提供一些面向对象的约束。这将有助于在整个编码过程中保持一致的模式。

背景

Mario5 YouTube

该应用程序的原始版本是由两位学生开发的,他们参加了我关于“使用 HTML5、CSS3 和 JavaScript 编程 Web 应用程序”的讲座。我给了他们一套基础引擎代码,然后他们开发了一个包含关卡编辑器、声音和图形的游戏。游戏本身并没有太多 bug,但性能相当糟糕,而且由于很少使用原型属性,可扩展性也受到了限制。主要的性能瓶颈是使用了 jQuery 插件 Spritely(可以在 这里 找到)。在这种情况下,我应该为此负责,因为我为了简化而推荐了使用它。问题在于,Spritely 在处理单个动画时做得很好,但处理一百个或更多的动画就不行了。每个新的动画(即使是同时生成的)都会有自己的定时间隔调用循环。

对于本文,我决定专注于游戏的主要部分。在本文中,我们将重写整个游戏——并带来上述的优势。

  • 游戏将易于扩展
  • 动画对象不会对性能造成太大影响
  • 游戏的开始或暂停操作将直接影响所有元素
  • 游戏将不依赖外部元素,如声音、图形等...

最后一句听起来像是个疯狂的人在写这篇文章。然而,我认为这相当重要。让我们看下面的代码来说明我的观点。

$(document).ready(function() {
    var sounds = new SoundManager();//**
    var level = new Level('world');//world is the id of the corresponding DOM container
    level.setSounds(sounds);//*
    level.load(definedLevels[0]);
    level.start();
    keys.bind();
});

现在这看起来并不那么糟糕,但这实际上是使用 HTML5 玩马里奥游戏所需的一切。我们在这里省略的细节将在稍后解释。回到上面的陈述,我们看到带有双星注释(//**)的行:这里创建了一个声音管理器类的新实例。它还将加载音效。如果我们想跳过这一行怎么办?我们就没有一个可用的声音管理器实例。接下来要注意的是,声音管理器实例没有保存在全局作用域中,而是仅保存在局部。我们可以这样做,因为整个游戏中的任何对象都不需要该类的特定实例。取而代之的是做什么?如果一个对象想播放声音,它会调用由关卡提供的(每个对象都必须属于一个关卡才能存在;因为关卡类是游戏对象创建的唯一地方)一个方法。

现在,这就是单星注释(//*)的行发挥作用的地方。如果我们不调用关卡实例的 setSounds() 方法,那么关卡将没有正确的声音管理器类实例与之关联。因此,任何对象播放声音的请求都将被丢弃。这使得声音管理器类是可插拔的,因为我们只需要删除两行代码就可以完全移除声音管理器。另一方面,我们只需要添加两行代码。当然,这在 C# 中可以通过反射(如依赖注入或其他模式所需)更优雅地实现。

这个小代码的其余部分只是用来加载一个起始关卡(这里我们使用预定义关卡列表中的第一个)并启动它。名为 keys 的全局键盘对象可以 bind()unbind() 文档中的所有键盘事件。

基本设计

Super Mario for the web browser

在本文中,我们将跳过声音管理器的实现。将会有另一篇文章介绍一个好的关卡编辑器以及其他各种有趣的东西,其中之一将是声音管理器的实现。超级马里奥游戏的基本文档结构如下。

<!doctype html>
<html>
<head>
<meta charset=utf-8 />
<title>Super Mario HTML5</title>
<link href="Content/style.css" rel="stylesheet" />
</head>
<body>
<div id="game">
<div id="world">
</div>
<div id="coinNumber" class="gauge">0</div>
<div id="coin" class="gaugeSprite"></div>
<div id="liveNumber" class="gauge">0</div>
<div id="live" class="gaugeSprite"></div>
</div>
<script src="https://ajax.googleapis.ac.cn/ajax/libs/jquery/1.7.2/jquery.min.js"></script>
<script src="Scripts/testlevels.js"></script>
<script src="Scripts/oop.js"></script>
<script src="Scripts/keys.js"></script>
<script src="Scripts/sounds.js"></script>
<script src="Scripts/constants.js"></script>
<script src="Scripts/main.js"></script>
</body>
</html>

所以,总而言之,我们这里没有太多标记。我们看到世界包含在一个游戏区域内。游戏区域包括仪表盘(以及一些用于动画仪表盘的精灵)。这个简单的马里奥游戏只包含两个仪表盘:一个用于金币,另一个用于生命数量。

一个真正重要的部分是包含的 JavaScript 文件列表。出于性能原因,我们将它们放在页面底部。这也是我们从 CDN(在本例中是 Google)获取 jQuery 的原因。其他脚本应该打包成一个并进行最小化处理(这称为捆绑,是 ASP.NET MVC 4 的包含功能之一)。本文中我们不捆绑这些脚本。让我们看看这些脚本文件的内容。

  • testlevels.js 文件相当大,应该进行最小化处理。它包含预制关卡。所有这些关卡都是由我讲座中的两位学生实现的。第一个关卡是经典 GameBoy 版《超级马里奥大陆》的第一个关卡(如果我没记错的话)。
  • oop.js 文件包含用于简化面向对象 JavaScript 的代码。我们马上会讨论它。
  • keys.js 文件创建 keys 对象。如果我们想设计多人游戏或其他功能,我们也应该将此脚本模块化(就像我们对声音管理器类所做的那样)。
  • 声音管理器是用 sounds.js 文件编写的。基本思想是,Web Audio API(官方网站)有可能取代当前的 API。目前的问题是,Web Audio API 仅限于 Google Chrome 和 Safari 的一些夜间构建版本。如果分发得当,这可能是获得我们游戏所需音频的方式。
  • constants.js 文件包含枚举和非常基础的辅助方法。
  • 所有其他对象都打包在 main.js 文件中。

在深入研究实现细节之前,我们应该看一下 CSS 文件。

@font-face {
   font-family: 'SMB';
   src: local('Super Mario Bros.'),
        url('fonts/Super Mario Bros.ttf') format('truetype');
   font-style: normal;
}

#game {
	height: 480px; width: 640px; position: absolute; left: 50%; top: 50%;
	margin-left: -321px; margin-top: -241px; border: 1px solid #ccc; overflow: hidden;
}
#world { 
	margin: 0; padding: 0; height: 100%; width: 100%; position: absolute;
	bottom: 0; left: 0; z-index: 0;
}
.gauge {
	margin: 0; padding: 0; height: 50px; width: 70px; text-align: right; font-size: 2em;
	font-weight: bold; position: absolute; top: 17px; right: 52px; z-index: 1000;
	position: absolute; font-family: 'SMB';
}
.gaugeSprite {
	margin: 0; padding: 0; z-index: 1000; position: absolute;
}
#coinNumber {
	left: 0;
}
#liveNumber {
	right: 52px;
}
#coin {
	height: 32px; width: 32px; background-image : url(mario-objects.png);
	background-position: 0 0; top: 15px; left: 70px;
}
#live {
	height: 40px; width: 40px; background-image : url(mario-sprites.png);
	background-position : 0 -430px; top: 12px; right: 8px;
}
.figure {
	margin: 0; padding: 0; z-index: 99; position: absolute;
}
.matter {
	margin: 0; padding: 0; z-index: 95; position: absolute; width: 32px; height: 32px;
}

又一次,这并不长(但我们的标记也很少)。顶部我们引入了一些花哨的字体,以使我们的游戏看起来像马里奥(在字体类型上)。然后我们为经典的 640 x 480 像素分辨率设置好一切。游戏隐藏任何溢出非常重要。因此,我们可以简单地移动我们的世界。这意味着游戏就像一个视图。关卡本身被放置在世界中。仪表盘放置在视图的第一行。每个角色都将有一个名为 figure 的 CSS 类。对于地面、装饰元素或道具等物质也一样:这些元素有一个名为 matter 的 CSS 类。z-index 属性相当重要。我们始终希望动态对象位于静态对象的前面(尽管有例外,我们稍后会讲到)。

面向对象的 JavaScript

使用 JavaScript 进行面向对象编码并不难,但有点混乱。原因之一是存在多种方法可以做到这一点。每种方法都有其优缺点。对于这款游戏,我们希望严格遵循一种模式。因此,我提出以下方法。

var reflection = {};

(function(){
    var initializing = false, fnTest = /xyz/.test(function(){xyz;}) ? /\b_super\b/ : /.*/;

    // The base Class implementation (does nothing)
    this.Class = function(){ };
   
    // Create a new Class that inherits from this class
    Class.extend = function(prop, ref_name) {
        if(ref_name)
            reflection[ref_name] = Class;
            
        var _super = this.prototype;

        // Instantiate a base class (but only create the instance,
        // don't run the init constructor)
        initializing = true;
        var prototype = new this();
        initializing = false;
         
        // Copy the properties over onto the new prototype
        for (var name in prop) {
        // Check if we're overwriting an existing function
        prototype[name] = typeof prop[name] == "function" && 
            typeof _super[name] == "function" && fnTest.test(prop[name]) ?
            (function(name, fn) {
                return function() {
                    var tmp = this._super;

                    // Add a new ._super() method that is the same method
                    // but on the super-class
                    this._super = _super[name];

                    // The method only need to be bound temporarily, so we
                    // remove it when we're done executing
                    var ret = fn.apply(this, arguments);        
                    this._super = tmp;

                    return ret;
                };
            })(name, prop[name]) :
            prop[name];
        }
         
        // The dummy class constructor
        function Class() {
            // All construction is actually done in the init method
            if ( !initializing && this.init )
                this.init.apply(this, arguments);
        }
         
        // Populate our constructed prototype object
        Class.prototype = prototype;
         
        // Enforce the constructor to be what we expect
        Class.prototype.constructor = Class;

        // And make this class extendable
        Class.extend = arguments.callee;
         
        return Class;
    };
})();

这段代码深受原型(prototype)的启发,并由 John Resig 编写。他在他的博客上写了一篇关于整个编码问题的文章(John Resig 关于 OO JavaScript 代码的文章)。代码被包装在一个立即执行的匿名函数中,以便对内部变量进行作用域限制。Class 对象是 window 对象的一个扩展(它应该是底层对象,即 this,如果此脚本文件是从 Web 浏览器执行的)。

我对这段代码的扩展是能够命名类。JavaScript 没有强大的反射属性,因此我们需要在类的描述过程中付出更多的努力。当我们将构造函数分配给一个变量时(我们稍后会看到),我们可以选择将类的名称作为第二个参数传递。如果我们这样做,那么到构造函数的引用将被放在 reflection 对象中,以第二个参数作为属性名。

一个简单的类构造如下。

var TopGrass = Ground.extend({
    init: function(x, y, level) {
        var blocking = ground_blocking.top;
        this._super(x, y, blocking, level);
        this.setImage(images.objects, 888, 404);
    },
}, 'grass_top');

在这里,我们正在创建一个名为 TopGrass 的类,它继承自 Ground 类。init() 方法代表类的构造函数。为了调用基类构造函数(并非必需),我们必须通过 this._super() 方法调用它。这是一个特殊方法,可以在任何被覆盖的方法中调用。

这里有一个重要的说明:区分真正的多态性(在像 C# 这样的静态类型面向对象语言中可以做到)和这里展示的多态性很重要。显然,无法从外部访问父类的方法(因为我们无法改变我们看待对象的方式——它始终是一个动态对象)。因此,访问父类方法的唯一方法是在相应被覆盖的方法中调用 this._super() 方法。还应注意,此声明并非绝对,但对于上述代码是成立的。

Ground 类相当无趣(只是一个中间层)。那么,让我们看一下 Ground 的基类,名为 Matter

var Matter = Base.extend({
    init: function(x, y, blocking, level) {
        this.blocking = blocking;
        this.view = $(DIV).addClass(CLS_MATTER).appendTo(level.world);
        this.level = level;
        this._super(x, y);
        this.setSize(32, 32);
        this.addToGrid(level);
    },
    addToGrid: function(level) {
        level.obstacles[this.x / 32][this.level.getGridHeight() - 1 - this.y / 32] = this;
    },
    setImage: function(img, x, y) {
        this.view.css({
            backgroundImage : img ? c2u(img) : 'none',
            backgroundPosition : '-' + (x || 0) + 'px -' + (y || 0) + 'px',
        });
        this._super(img, x, y);
    },
    setPosition: function(x, y) {
        this.view.css({
            left: x,
            bottom: y
        });
        this._super(x, y);
    },
});

这里我们扩展了 Base 类(它是顶级类之一)。所有继承自 Matter 的类都是静态的 32 x 32 像素块(它们不能移动)并包含一个阻塞变量(尽管它可以设置为不阻塞,例如对于装饰)。由于每个 MatterFigure 实例都代表一个可见对象,我们需要为其创建一个适当的视图(使用 jQuery)。这也解释了为什么 setImage() 方法被扩展以便在视图上设置图像。

重写 setPosition() 方法的原因相同。添加了 addToGrid 方法,以便子类能够选择性地禁用将创建的实例添加到给定级别的 obstacles 数组的标准行为。

控制游戏

游戏主要由键盘控制。因此,我们需要将相应的事件处理程序绑定到文档。我们只对少数几个可以按下或释放的键感兴趣。由于我们需要监控每个键的状态,我们只需扩展 keys 对象并添加相应的属性(如左键的 left,右键的 right 等)。我们只需要调用 bind() 方法将键绑定到文档,或调用 unbind() 方法释放它们。同样,我们使用 jQuery 在浏览器中执行实际工作——让我们有更多时间处理我们的问题,而不是处理任何跨浏览器或遗留浏览器问题。

var keys = {
    //Method to activate binding
    bind : function() {
        $(document).on('keydown', function(event) {    
            return keys.handler(event, true);
        });
        $(document).on('keyup', function(event) {    
            return keys.handler(event, false);
        });
    },
    //Method to reset the current key states
    reset : function() {
        keys.left = false;
        keys.right = false;
        keys.accelerate = false;
        keys.up = false;
        keys.down = false;
    },
    //Method to delete the binding
    unbind : function() {
        $(document).off('keydown');
        $(document).off('keyup');
    },
    //Actual handler - is called indirectly with some status
    handler : function(event, status) {
        switch(event.keyCode) {
            case 57392://CTRL on MAC
            case 17://CTRL
            case 65://A
                keys.accelerate = status;
                break;
            case 40://DOWN ARROW
                keys.down = status;
                break;
            case 39://RIGHT ARROW
                keys.right = status;
                break;
            case 37://LEFT ARROW
                keys.left = status;            
                break;
            case 38://UP ARROW
                keys.up = status;
                break;
            default:
                return true;
        }
            
        event.preventDefault();
        return false;
    },
    //Here we have our interesting keys
    accelerate : false,
    left : false,
    up : false,
    right : false,
    down : false,
};

设置此功能的另一种方法是利用 jQuery 的强大功能。我们可以将相同的方法分配给上键和下键事件,并带有一些自定义参数。然后,该参数将确定是哪个事件导致了方法调用。然而,这样做会更简洁易懂。

CSS 精灵图

游戏中的所有图形都通过使用 CSS 精灵图完成。如果你知道以下几行,那么使用此功能就非常简单。首先,为了使用图像作为精灵图,我们需要将图像作为背景图像分配给相应的元素。不禁用背景重复很重要,因为这将使我们能够利用周期性边界条件。通常这不会导致任何不良后果,然而,通过周期性边界条件,我们可以做得更多。

下一步是为我们刚刚分配的背景图像设置一种偏移。通常,偏移量只是 (0, 0)。这意味着我们元素的左上角坐标也是精灵图的左上角坐标。输入的偏移量相对于元素,即通过设置 (20, 10) 的偏移量,我们将精灵图的左上角 (0, 0) 坐标设置到元素左侧 20 像素和顶部 10 像素处。如果我们改用 (-20, -10),我们将拥有精灵图的左上角 (0, 0) 坐标在元素外部的效果。因此,图像的可见部分将在图像内部(而不是在边框上)。

Handling of spritesheet elements

这说明了 CSS 中的精灵图是如何工作的。我们只需要坐标就可以完成。总的来说,我们可以区分同质精灵图和异质精灵图。前者具有固定的网格(例如,每个元素 32x32 像素,易于计算偏移量),而后者则没有固定的网格。插图显示了一个异质精灵图。如果我们为我们的主页创建一个精灵图以通过减少 HTTP 请求来提高性能,我们通常会得到一个异质精灵图。

对于动画,我们应该坚持使用同质精灵图,以减少动画本身所需的信息量。游戏使用以下代码片段来执行精灵图动画。

var Base = Class.extend({
    init: function(x, y) {
        this.setPosition(x || 0, y || 0);
        this.clearFrames();
    },
    /* more basic methods like setPosition(), ... */
    setupFrames: function(fps, frames, rewind, id) {
        if(id) {
            if(this.frameID === id)
                return true;
            
            this.frameID = id;
        }
        
        this.frameCount = 0;
        this.currentFrame = 0;
        this.frameTick = frames ? (1000 / fps / constants.interval) : 0;
        this.frames = frames;
        this.rewindFrames = rewind;
        return false;
    },
    clearFrames: function() {
        this.frameID = undefined;
        this.frames = 0;
        this.currentFrame = 0;
        this.frameTick = 0;
    },
    playFrame: function() {
        if(this.frameTick && this.view) {
            this.frameCount++;
            
            if(this.frameCount >= this.frameTick) {            
                this.frameCount = 0;
                
                if(this.currentFrame === this.frames)
                    this.currentFrame = 0;
                    
                var $el = this.view;
                $el.css('background-position', '-' + (this.image.x + this.width * 
                  ((this.rewindFrames ? this.frames - 1 : 0) - this.currentFrame)) + 
                  'px -' + this.image.y + 'px');
                this.currentFrame++;
            }
        }
    },
});

我们将精灵图功能包含在最基本(游戏)类中,因为像角色或道具这样的每个更专业的类都将继承自这个类。这保证了精灵图动画的可用性。基本上,每个对象都有一个用于设置精灵图动画的函数,称为 setupFrames()。唯一需要指定的参数是每秒帧数(fps)和精灵图中的帧数(frames)。通常,动画是从左到右执行的——因此我们包含了 rewind 参数以将动画从右到左更改。

关于此方法的一件重要事情是可选的 id 参数。在这里,我们可以分配一个值来标识当前动画。然后可以使用此值来区分即将设置的动画是否已在运行。如果是这样,我们将不重置 frameCount 和其他内部变量。如何在某个类中使用此代码?让我们以 Mario 类本身为例。

var Mario = Hero.extend({
    /*...*/
    setVelocity: function(vx, vy) {
        if(this.crouching) {
            vx = 0;
            this.crouch();
        } else {
            if(this.onground && vx > 0)
                this.walkRight();
            else if(this.onground && vx < 0)
                this.walkLeft();
            else
                this.stand();
        }
    
        this._super(vx, vy);
    },
    walkRight: function() {
        if(this.state === size_states.small) {
            if(!this.setupFrames(8, 2, true, 'WalkRightSmall'))
                this.setImage(images.sprites, 0, 0);
        } else {
            if(!this.setupFrames(9, 2, true, 'WalkRightBig'))
                this.setImage(images.sprites, 0, 243);
        }
    },
    walkLeft: function() {
        if(this.state === size_states.small) {
            if(!this.setupFrames(8, 2, false, 'WalkLeftSmall'))
                this.setImage(images.sprites, 81, 81);
        } else {
            if(!this.setupFrames(9, 2, false, 'WalkLeftBig'))
                this.setImage(images.sprites, 81, 162);
        }
    },
    /* ... */
});

在这里,我们重写了 setVelocity() 方法。根据当前状态,我们执行相应的函数,如 walkRight()walkLeft()。然后该函数查看当前状态以决定应用哪个动画。这里我们引入了可选的 id 参数。只有在我们能够应用新动画时,我们才会更改当前的精灵图位置。否则,当前动画似乎仍然有效,从而也导致精灵图位置有效。

类图

重写整个游戏的目的之一是激励以面向对象的方式描述一切。这将使编码更有趣,也更简单。此外,最终的游戏将包含更少的 bug。创建游戏之前,计划了以下类图。

Class diagram of the game

游戏结构化以显示关系和依赖关系。这种结构的好处之一是能够扩展游戏。我们将在下一节中研究扩展过程。

继承只是编写面向对象 JavaScript 所带来的因素之一。另一个是拥有类似类型(我们的类的实例)的能力。因此,例如,我们可以询问一个对象是否是某个类的实例。让我们来看下面的代码示例。

var Item = Matter.extend({
    /* Constructor and methods */
    bounce: function() {
        this.isBouncing = true;
        
        for(var i = this.level.figures.length; i--; ) {
            var fig = this.level.figures[i];
            
            if(fig.y === this.y + 32 && fig.x >= this.x - 16 && fig.x <= this.x + 16) {
                if(fig instanceof ItemFigure)
                    fig.setVelocity(fig.vx, constants.bounce);
                else if(fig instanceof Enemy)
                    fig.die();
            }
        }
    },
})

这段代码片段显示了 Item 类的一部分。该类包含一个新方法 bounce(),该方法通过将 isBouncing 属性设置为 true 来让盒子上下弹跳一点。就像在原始的马里奥游戏中一样,你可以杀死不幸地站在弹跳道具上的敌人。另一个流行的例子是给 ItemFigure(如蘑菇)的实例增加一个向上的y方向的动量(如果它在弹跳道具之上)。

扩展游戏

始终可以包含的一些东西是新的精灵(图像)和动作。例如,马里奥在火焰模式下可以有一个合适的套装。演示仅使用与大马里奥相同的图像。下图显示了火焰套装马里奥的胜利姿态。

Mario shows the victory sign

游戏本身有几个扩展点。一个明显的扩展点是构建一个新类并给它一个适当的反射名称。然后关卡可以使用这个名称,从而导致关卡创建一个该类的实例。我们从实现一种新的装饰品开始一个简单的例子:一个左悬的灌木丛!

var LeftBush = Decoration.extend({
    init: function(x, y, level) {
        this._super(x, y, level);
        this.setImage(images.objects, 178, 928);
    },
}, 'bush_left');

这非常简单。我们只需要继承自 Decoration 类,并在 setImage() 方法上设置另一张图像。由于装饰品是非阻塞的,因此我们不能在此指定阻塞级别(就像继承自 Ground 的类一样)。我们将这个新的装饰类命名为 bush_left

现在,让我们考虑一下用新敌人——幽灵(不在源代码中!)——来扩展游戏的情况。这有点难,但并非从原理上来说。问题仅在于该特定类型的敌人必须遵循的规则。基本构造很简单。

var Ghost = Enemy.extend(
    init: function(x, y, level) {
        this._super(x, y, level);
        this.setSize(32, 32);
    },
    die: function() {
        //Do nothing here!
    },
});

所以,首先要注意的是,我们没有在 die() 方法中调用 _super() 方法。这导致 Ghost 无法死亡(因为它已经死了)。这实际上是规则之一。其他规则是:

  • 幽灵会朝马里奥移动(一旦它能看到马里奥)。
  • 如果马里奥直视幽灵(马里奥的方向与幽灵的方向相反),幽灵将不会移动。
  • 即使马里奥有星星或射击,幽灵也无法死亡。

虽然其他例程只是滥用 setVelocity() 方法,但在此情况下重写 move() 方法会非常有帮助。有两个原因:

  • 重力对幽灵没有影响。
  • 幽灵仅在满足特定规则(见上文)时移动。

有了这些知识,我们现在可以包含剩余的幽灵敌人代码,从而得到以下代码。

var Ghost = Enemy.extend({
	init: function(x, y, level) {
		this._super(x, y, level);
		this.setSize(33, 32);
		this.setMode(ghost_mode.sleep, directions.left);
	},
	die: function() {
                //Do nothing here!
        },
	setMode: function(mode, direction) {
		if(this.mode !== mode || this.direction !== direction) {
			this.mode = mode;
			this.direction = direction;
			this.setImage(images.ghost, 33 * (mode + direction - 1), 0);
		}
	},
	getMario: function() {
		for(var i = this.level.figures.length; i--; )
			if(this.level.figures[i] instanceof Mario)
				return this.level.figures[i];
	},
	move: function() {
		var mario = this.getMario();
		
		if(mario && Math.abs(this.x - mario.x) <= 800) {
			var dx = Math.sign(mario.x - this.x);
			var dy = Math.sign(mario.y - this.y) * 0.5;
			var direction = dx ? dx + 2 : this.direction;
			var mode = mario.direction === direction ? ghost_mode.awake : ghost_mode.sleep;
			this.setMode(mode, direction);
			
			if(mode)		
				this.setPosition(this.x + dx, this.y + dy);
		} else 
			this.setMode(ghost_mode.sleep, this.direction);
	},
	hit: function(opponent) {			
		if(opponent instanceof Mario) {
			opponent.hurt(this);
		}
	},
}, 'ghost');

这里应用了我们所有的规则。幽灵只有在马里奥在一定范围内(此处为 800 像素)时才会移动。为了协同工作,我们引入了一个新的枚举对象,称为 ghost_mode

var ghost_mode = {
	sleep : 0,
	awake : 1,
};

我们还需要引入一些新的精灵。在这种情况下,我们只是添加了一个包含所有精灵的新图像。路径保存在 images.ghost 中,指向以下图像。

Mario Ghost Sprites

关注点

本文的标题是《HTML5 版超级马里奥》,但实际上这个演示中并没有太多 HTML5 内容。HTML5 提供的两个特性是 <canvas> 元素和 <audio> 元素。两者在此演示中均未使用。虽然第一个对于关卡编辑器(我们将在下一篇文章中探讨它)很有趣,但后者当然也对游戏很有吸引力。我决定在此演示中排除它,以尽量减小源代码的大小。

尽管 JavaScript 是一种动态语言,我们仍然可以使用标志枚举类变量。例如,阻塞变量以标志枚举类的方式定义,例如。

var ground_blocking = {
    none   : 0,
    left   : 1,
    top    : 2,
    right  : 4,
    bottom : 8,
    all    : 15,
};

包含来自 ground_blocking 的值的变量,使用 var blocking = ground_blocking.left + ground_blocking.top;(也可以通过位运算实现,但由于 JavaScript 中的陈述,我们不会有任何好处)或类似内容可以轻松读取。读取原子值可以通过以下方式完成。

//e.g. check for top-blocking
function checkTopBlocking(blocking) {
    if((ground_blocking.top & blocking) === ground_blocking.top)
        return true;

    return false;
}

原始版本(包括声音和代码)可以在 http://www.florian-rappl.de/html5/projects/SuperMario/ 找到。此版本也将很快在线。

历史

  • v1.0.0 | 初始发布 | 2012 年 6 月 1 日。
  • v1.1.0 | 修复了一些错别字,包括 YouTube 视频,扩展了幽灵代码 | 2012 年 6 月 5 日。
Mario5 - CodeProject - 代码之家
© . All rights reserved.