炸弹人游戏开发系列(5):控制炸弹人移动,引入状态模式

前言css

上文中咱们实现了炸弹人显示和左右移动。本文开始监听键盘事件,使玩家能控制炸弹人移动。而后会在重构的过程当中会引入状态模式。你们会看到我是如何在开发的过程当中经过重构来提出设计模式,而不是在初步设计阶段提出设计模式的。html

本文目的

实现“使用键盘控制玩家移动”canvas

完善炸弹人移动,增长上下方向的移动设计模式

本文主要内容

回顾上文更新后的领域模型

开发策略性能优化

首先进行性能优化,使用双缓冲技术显示地图。接着考虑到“增长上下移动”的功能与上文实现的“左右移动”功能相似,实现起来没有难度,所以优先实现“使用键盘控制玩家移动”,再实现“增长上下移动”。app

性能优化ide

双缓冲

什么是双缓冲

当数据量很大时,绘图可能须要几秒钟甚至更长的时间,并且有时还会出现闪烁现象,为了解决这些问题,可采用双缓冲技术来绘图。
双缓冲即在内存中建立一个与屏幕绘图区域一致的对象,先将图形绘制到内存中的这个对象上,再一次性将这个对象上的图形拷贝到屏幕上,这样能大大加快绘图的速度。双缓冲实现过程以下:
一、在内存中建立与画布一致的缓冲区
二、在缓冲区画图
三、将缓冲区位图拷贝到当前画布上
四、释放内存缓冲区

为何要用双缓冲

由于显示地图是这样显示的:假设地图大小为40*40,每一个单元格是一个bitmap,则有40*40个bitmap。使用canvas的drawImage绘制每一个bitmap,则要绘制40*40次才能绘制完一张完整的地图,开销很大。函数

那么应该如何优化呢?post

  • 每次只绘制地图中变化的部分。
  • 当变化的范围也很大时(涉及到多个bitmap),则可用双缓冲,减少页面抖动的现象。

所以,使用“分层渲染”能够实现第1个优化,而使用“双缓冲”则可实现第2个优化。性能

实现

在MapLayer中建立一个缓冲画布,在绘制地图时先在缓冲画布上绘制,绘制完成后再将缓冲画布拷贝到地图画布中。

MapLayer

(function () {
    var MapLayer = YYC.Class(Layer, {
        Init: function () {
            //*双缓冲

            //建立缓冲canvas
            this.___createCanvasBuffer();
            //得到缓冲context
            this.___getContextBuffer();
        },
        Private: {
            ___canvasBuffer: null,
            ___contextBuffer: null,

            ___createCanvasBuffer: function () {
                this.___canvasBuffer = $("<canvas/>", {
                    width: bomberConfig.canvas.WIDTH.toString(),
                    height: bomberConfig.canvas.HEIGHT.toString()
                })[0];
            },
            ___getContextBuffer: function () {
                this.___contextBuffer = this.___canvasBuffer.getContext("2d");
            },
            ___drawBuffer: function (img) {
                this.___contextBuffer.drawImage(img.img, img.x, img.y, img.width, img.height);
            }
        },
        Protected: {
            P__createCanvas: function () {
                var canvas = $("<canvas/>", {
                    width: bomberConfig.canvas.WIDTH.toString(),
                    height: bomberConfig.canvas.HEIGHT.toString(),
                    css: {
                        "position": "absolute",
                        "top": bomberConfig.canvas.TOP,
                        "left": bomberConfig.canvas.LEFT,
                        "border": "1px solid blue",
                        "z-index": 0
                    }
                });
                $("body").append(canvas);

                this.P__canvas = canvas[0];
            }
        },
        Public: {
            draw: function () {
                var i = 0,
                    len = 0,
                    imgs = null;

                imgs = this.getChilds();

                for (i = 0, len = imgs.length; i < len; i++) {
                    this.___drawBuffer(imgs[i]);
                }
                this.P__context.drawImage(this.___canvasBuffer, 0, 0);
            },
            clear: function () {
                this.___contextBuffer.clearRect(0, 0, bomberConfig.canvas.WIDTH, bomberConfig.canvas.HEIGHT);
                this.base();
            },
            render: function () {
                if (this.P__isChange()) {
                    this.clear();
                    this.draw();
                    this.P__setStateNormal();
                }
            }
        }
    });

    window.MapLayer = MapLayer;
}());

控制炸弹人移动

如今,让咱们来实现“使用键盘控制炸弹人家移动” 。

分离出KeyEventManager类

由于玩家是经过键盘事件来控制炸弹人的,因此考虑提出一个专门处理事件的KeyEventManager类,它负责键盘事件的绑定与移除。

提出按键枚举值

由于控制炸弹人移动的方向键能够为W、S、A、D,也能够为上、下、左、右方向键。也就是说,具体的方向键可能根据我的喜爱变化,能够提供几套方向键方案,让玩家本身选择。

为了实现上述需求,须要使用枚举值KeyCodeMap来代替具体的方向键。这样有如下好处:

  • 使用抽象隔离具体变化。当具体的方向键变化时,只要改变枚举值对应的value便可,而枚举值不会变化
  • 增长可读性。枚举值如Up一看就知道表示向上走,而87(W键的keycode)则看不出来是什么意思。

增长keystate

若是在KeyEventManager绑定的键盘事件中直接操做PlayerSprite:

  • 耦合过重。PlayerSprite变化时也会影响到KeyEventManager
  • 不够灵活。若是之后增长多个玩家的需求,那么就须要修改KeyEventManager,使其直接操做多个玩家精灵类,这样耦合会更中,第一点的状况也会更严重。

所以,我增长按键状态keyState。这是一个空类,用于存储当前的按键状态。

当触发键盘事件时,KeyEventManager类改变keyState。而后在须要处理炸弹人移动的地方(如PlayerSprite),判断keyState,就能够知道当前按下的是哪一个键,进而控制炸弹人进行相应方向的移动。

领域模型

相关代码

KeyCodeMap

var keyCodeMap = {
    Left: 65, // A键
    Right: 68, // D键
    Down: 83, // S键
    Up: 87 // W键
};

KeyEventManager、KeyState

(function () {
    //枚举值
    var keyCodeMap = {
        Left: 65, // A键
        Right: 68, // D键
        Down: 83, // S键
        Up: 87 // W键
    };
    //按键状态
    var keyState = {};


    var KeyEventManager = YYC.Class({
        Private: {
            _keyDown: function () { },
            _keyUp: function () { },
            _clearKeyState: function () {
                window.keyState = {};
            }
        },
        Public: {
            addKeyDown: function () {
                var self = this;

                this._keyDown = YYC.Tool.event.bindEvent(this, function (e) {
                    self._clearKeyState();

                    window.keyState[e.keyCode] = true;
                });

                YYC.Tool.event.addEvent(document, "keydown", this._keyDown);
            },
            removeKeyDown: function(){
                YYC.Tool.event.removeEvent(document, "keydown", this._keyDown);
            },
            addKeyUp: function () {
                var self = this;

                this._keyUp = YYC.Tool.event.bindEvent(this, function (e) {
                    self._clearKeyState();

                    window.keyState[e.keyCode] = false;
                });

                YYC.Tool.event.addEvent(document, "keyup", this._keyUp);
            },
            removeKeyUp: function () {
                YYC.Tool.event.removeEvent(document, "keyup", this._keyUp);
            },
        }
    });

    window.keyCodeMap = keyCodeMap;
    window.keyState = keyState;
    window.keyEventManager = new KeyEventManager();
}());

PlayerSprite

            handleNext: function () {
                if (window.keyState[keyCodeMap.A] === true) {
                    this.speedX = -this.speedX;
                    this.setAnim("walk_left");
                }
                else if (window.keyState[keyCodeMap.D] === true) {
                    this.speedX = this.speedX;
                    this.setAnim("walk_right");
                }
                else {
                    this.speedX = 0;
                    this.setAnim("stand_right");
                }
            }

在游戏初始化时绑定事件:

Game

        _initEvent: function () {
            keyEventManager.addKeyDown();
            keyEventManager.addKeyUp();
        }
        ...
        init: function () {
            ...
            this._initEvent();
        },

引入状态模式

发现“炸弹人移动”中,存在不一样状态,且状态能够转换的现象

在上一篇博文中,我实现了显示和移动炸弹人,炸弹人能够在画布上左右走动。

我发如今游戏中,炸弹人是处于不一样的状态的:站立、走动。又能够将状态具体为:左站、右站、左走、右走。

炸弹人处于不一样状态时,它的行为是不同的(如处于左走状态时,炸弹人移动方向为向左;处于右走状态时,炸弹人移动方向为向右),且不一样状态之间能够转换。

状态图

根据上面的分析,让我萌生了可使用状态模式的想法。 状态模式介绍详见Javascript设计模式之我见:状态模式

为何在此处用状态模式

其实此处炸弹人的状态数并很少,且每一个状态的逻辑也不复杂,彻底能够直接在PlayerState中使用if else来实现状态的逻辑和状态切换。

那为何我要用状态模式了?

一、作这个游戏是为了学习,状态模式我以前没有实际应用过,所以能够在此处练手

二、此处也符合状态模式的应用场景:一个对象的行为取决于它的状态, 而且它必须在运行时刻根据状态改变它的行为

三、扩展方便。目前实现了炸弹人左右移动,后面还会实现炸弹人上下移动。若是用状态模式的话,只须要增长四个状态:上走、上站、下走、下站,再对应修改Context和客户端便可。

应用状态模式的领域模型

 

状态模式具体实现 

由于有右走、右站、左走、左站四个状态类,所以就要建立4个具体状态类,分别对应这四个状态类。 

PlayerSprite

(function () {
    var PlayerSprite = YYC.Class(Sprite, {
        Init: function (data) {
            this.x = data.x;
            this.speedX = data.speedX;
            this.walkSpeed = data.walkSpeed;
            this.minX = data.minX;
            this.maxX = data.maxX;
            this.defaultAnimId = data.defaultAnimId;
            this.anims = data.anims;

            this.setAnim(this.defaultAnimId);

            this.__context = new Context(this);

            this.__context.setPlayerState(this.__getCurrentState());
        },
        Private: {
            __context: null,

            _getCurrentState: function () {
                var currentState = null;

                switch (this.defaultAnimId) {
                    case "stand_right":
                        currentState = Context.standRightState;
                        break;
                    case "stand_left":
                        currentState = Context.standLeftState;
                        break;
                    case "walk_right":
                        currentState = Context.walkRightState;
                        break;
                    case "walk_left":
                        currentState = Context.walkLeftState;
                        break;
                    default:
                        throw new Error("未知的状态");
                        break;
                }
            }
        },
        Public: {
            //精灵的速度
            speedX: 0,
            speedY: 0,
            //定义sprite走路速度的绝对值
            walkSpeed: 0,

            // 更新精灵当前状态
            update: function (deltaTime) {
                //每次循环,改变一下绘制的坐标
                this.__setCoordinate(deltaTime);

                this.base(deltaTime);
            },
            draw: function (context) {
                var frame = null;

                if (this.currentAnim) {
                    frame = this.currentAnim.getCurrentFrame();

                    context.drawImage(this.currentAnim.getImg(), frame.x, frame.y, frame.width, frame.height, this.x, this.y, frame.imgWidth, frame.imgHeight);
                }
            },
            clear: function (context) {
                var frame = null;

                if (this.currentAnim) {
                    frame = this.currentAnim.getCurrentFrame();

                    //要加上图片的宽度/高度
                    context.clearRect(0, 0, this.maxX + frame.imgWidth, this.maxY + frame.imgHeight);
                }
            },
            handleNext: function () {
                this.__context.walkLeft();
                this.__context.walkRight();
                this.__context.stand();
            }
        }
    });

    window.PlayerSprite = PlayerSprite;
}());
View Code

Context

(function () {
    var Context = YYC.Class({
        Init: function (sprite) {
            this.sprite = sprite;
        },
        Private: {
            _state: null
        },
        Public: {
            sprite: null,

            setPlayerState: function (state) {
                this._state = state;
                //把当前的上下文通知到当前状态类对象中
                this._state.setContext(this);
            },
            walkLeft: function () {
                this._state.walkLeft();
            },
            walkRight: function () {
                this._state.walkRight();
            },
            stand: function () {
                this._state.stand();
            }
        },
        Static: {
            walkLeftState: new WalkLeftState(),
            walkRightState: new WalkRightState(),
            standLeftState: new StandLeftState(),
            standRightState: new StandRightState()
        }
    });

    window.Context = Context;
}());
View Code

 

PlayerState

(function () {
    var PlayerState = YYC.AClass({
        Protected: {
            P_context: null
        },
        Public: {
            setContext: function (context) {
                this.P_context = context;
            }
        },
        Abstract: {
            stand: function () { },
            walkLeft: function () { },
            walkRight: function () { }
        }
    });

    window.PlayerState = PlayerState;
}());
View Code

WalkLeftState

(function () {
    var WalkLeftState = YYC.Class(PlayerState, {
        Public: {
            stand: function () {
                if (window.keyState[keyCodeMap.A] === false) {
                    this.P_context.sprite.resetCurrentFrame(0);
                    this.P_context.setPlayerState(Context.standLeftState);
                }
            },
            walkLeft: function () {
                var sprite = null;

                if (window.keyState[keyCodeMap.A] === true) {
                    sprite = this.P_context.sprite;
                    sprite.speedX = -sprite.walkSpeed;
                    sprite.speedY = 0;
                    sprite.setAnim("walk_left");
                }
            },
            walkRight: function () {
            }
        }
    });

    window.WalkLeftState = WalkLeftState;
}());
View Code

StandLeftState

(function () {
    var StandLeftState = YYC.Class(PlayerState, {
        Public: {
            stand: function () {
                var sprite = null;
                
                if (window.keyState[keyCodeMap.A] === false) {
                    sprite = this.P_context.sprite;
                    sprite.speedX = 0;
                    sprite.setAnim("stand_left");
                }
            },
            walkLeft: function () {
                if (window.keyState[keyCodeMap.A] === true) {
                    this.P_context.sprite.resetCurrentFrame(0);
                    this.P_context.setPlayerState(Context.walkLeftState);
                }
            },
            walkRight: function () {
                if (window.keyState[keyCodeMap.D] === true) {
                    this.P_context.sprite.resetCurrentFrame(0);
                    this.P_context.setPlayerState(Context.walkRightState);
                }
            }
        }
    });

    window.StandLeftState = StandLeftState;
}());
View Code

WalkRightState

(function () {
    var WalkRightState = YYC.Class(PlayerState, {
        Public: {
            stand: function () {
                if (window.keyState[keyCodeMap.D] === false) {
                    this.P_context.sprite.resetCurrentFrame(0);
                    this.P_context.setPlayerState(Context.standRightState);
                }
            },
            walkLeft: function () {
            },
            walkRight: function () {
                var sprite = null;

                if (window.keyState[keyCodeMap.D] === true) {
                    sprite = this.P_context.sprite;
                    sprite.speedX = sprite.walkSpeed;
                    sprite.speedY = 0;
                    sprite.setAnim("walk_right");
                }
            }
        }
    });

    window.WalkRightState = WalkRightState;
}());
View Code

 

StandRightState

(function () {
    var StandRightState = YYC.Class(PlayerState, {
        Public: {
            stand: function () {
                var sprite = null;

                if (window.keyState[keyCodeMap.D] === false) {
                    sprite = this.P_context.sprite;
                    sprite.speedX = 0;
                    sprite.setAnim("stand_right");
                }
            },
            walkLeft: function () {
                if (window.keyState[keyCodeMap.A] === true) {
                    this.P_context.sprite.resetCurrentFrame(0);
                    this.P_context.setPlayerState(Context.walkLeftState);
                }
            },
            walkRight: function () {
                if (window.keyState[keyCodeMap.D] === true) {
                    this.P_context.sprite.resetCurrentFrame(0);
                    this.P_context.setPlayerState(Context.walkRightState);
                }
            }
        }
    });

    window.StandRightState = StandRightState;
}());
View Code

重构PlayerSprite

PlayerSprite重构前相关代码

        Init: function (data) {
            this.x = data.x;
            this.speedX = data.speedX;
            this.walkSpeed = data.walkSpeed;
            this.minX = data.minX;
            this.maxX = data.maxX;
            this.defaultAnimId = data.defaultAnimId;
            this.anims = data.anims;
this.setAnim(this.defaultAnimId); this.__context = new Context(this);
this.__context.setPlayerState(this.__getCurrentState()); },

从构造函数中分离出init

如今构造函数Init看起来有4个职责:

  • 读取参数
  • 设置默认动画
  • 建立Context实例,且由于状态类须要得到PlayerSprite类的成员,所以在建立Context实例时,将PlayerSprite的实例注入到Context中。
  • 设置当前默认状态。

在测试PlayerSprite时,发现难以测试。这是由于构造函数职责太多,形成了互相的干扰。

从较高的层面来看,如今构造函数作了两件事:

  • 读取参数
  • 初始化

所以,我将“初始化”提出来,造成init方法。

构造函数保留“建立Context实例”职责

这里比较难决定的是“建立Context实例”这个职责应该放到哪里。

考虑到PlayerSprite与Context属于组合关系,Context只属于PlayerSprite,它应该在建立PlayerSprite时而建立。所以,将“建立Context实例”保留在PlayerSprite的构造函数中。

重构后的PlayerSprite

Init: function (data) {
    this.x = data.x;
    this.speedX = data.speedX;
    this.walkSpeed = data.walkSpeed;
    this.minX = data.minX;
    this.maxX = data.maxX;
    this.defaultAnimId = data.defaultAnimId;
    this.anims = data.anims;

    this._context = new Context(this);
},
...
    init: function () {
        this._context.setPlayerState(this._getCurrentState());

        this.setAnim(this.defaultAnimId);
    },
... 

增长炸弹人上下方向的移动

增长状态类

增长WalkUpState、WalkDownState、StandUpState、StandDownState类,并对应修改Context便可。

关于“为何要有四个方向的Stand状态类”的思考

看到这里,有朋友可能会说,为何用这么多的Stand状态类,直接用一个StandState类岂不是更简洁?

缘由在于,上站、下站、左站、右站的行为是不同的,这具体体如今显示的动画不同(炸弹人站立的方向不同)。

领域模型

相关代码

WalkUpState

(function () {
    var WalkUpState = YYC.Class(PlayerState, {
        Public: {
            stand: function () {
                if (window.keyState[keyCodeMap.W] === false) {
                    this.P_context.sprite.resetCurrentFrame(0);
                    this.P_context.setPlayerState(Context.standUpState);
                }
            },
            walkLeft: function () {
            },
            walkRight: function () {
            },
            walkUp: function () {
                var sprite = null;

                if (window.keyState[keyCodeMap.W] === true) {
                    sprite = this.P_context.sprite;
                    sprite.speedX = 0;
                    sprite.speedY = -sprite.walkSpeed;
                    sprite.setAnim("walk_up");
                }
            },
            walkDown: function () {
            }
        }
    });

    window.WalkUpState = WalkUpState;
}());
View Code

WalkDownState

(function () {
    var WalkDownState = YYC.Class(PlayerState, {
        Public: {
            stand: function () {
                if (window.keyState[keyCodeMap.S] === false) {
                    this.P_context.sprite.resetCurrentFrame(0);
                    this.P_context.setPlayerState(Context.standDownState);
                }
            },
            walkLeft: function () {
            },
            walkRight: function () {
            },
            walkUp: function () {
            },
            walkDown: function () {
                var sprite = null;

                if (window.keyState[keyCodeMap.S] === true) {
                    sprite = this.P_context.sprite;
                    sprite.speedX = 0;
                    sprite.speedY = sprite.walkSpeed;
                    sprite.setAnim("walk_down");
                }
            }
        }
    });

    window.WalkDownState = WalkDownState;
}());
View Code

StandUpState

(function () {
    var StandUpState = YYC.Class(PlayerState, {
        Public: {
            stand: function () {
                var sprite = null;
                if (window.keyState[keyCodeMap.W] === false) {
                    sprite = this.P_context.sprite;
                    
                    sprite.speedY = 0;
                    sprite.setAnim("stand_up");
                }
            },
            walkLeft: function () {
                if (window.keyState[keyCodeMap.A] === true) {
                    this.P_context.sprite.resetCurrentFrame(0);
                    this.P_context.setPlayerState(Context.walkLeftState);
                }
            },
            walkRight: function () {
                if (window.keyState[keyCodeMap.D] === true) {
                    this.P_context.sprite.resetCurrentFrame(0);
                    this.P_context.setPlayerState(Context.walkRightState);
                }
            },
            walkUp: function () {
                if (window.keyState[keyCodeMap.W] === true) {
                    this.P_context.sprite.resetCurrentFrame(0);
                    this.P_context.setPlayerState(Context.walkUpState);
                }
            },
            walkDown: function () {
                if (window.keyState[keyCodeMap.S] === true) {
                    this.P_context.sprite.resetCurrentFrame(0);
                    this.P_context.setPlayerState(Context.walkDownState);
                }
            }
        }
    });

    window.StandUpState = StandUpState;
}());
View Code

StandDownState

(function () {
    var StandDownState = YYC.Class(PlayerState, {
        Public: {
            stand: function () {
                var sprite = null;
                if (window.keyState[keyCodeMap.S] === false) {
                    sprite = this.P_context.sprite;
                    sprite.speedY = 0;
                    sprite.setAnim("stand_down");
                }
            },
            walkLeft: function () {
                if (window.keyState[keyCodeMap.A] === true) {
                    this.P_context.sprite.resetCurrentFrame(0);
                    this.P_context.setPlayerState(Context.walkLeftState);
                }
            },
            walkRight: function () {
                if (window.keyState[keyCodeMap.D] === true) {
                    this.P_context.sprite.resetCurrentFrame(0);
                    this.P_context.setPlayerState(Context.walkRightState);
                }
            },
            walkUp: function () {
                if (window.keyState[keyCodeMap.W] === true) {
                    this.P_context.sprite.resetCurrentFrame(0);
                    this.P_context.setPlayerState(Context.walkUpState);
                }
            },
            walkDown: function () {
                if (window.keyState[keyCodeMap.S] === true) {
                    this.P_context.sprite.resetCurrentFrame(0);
                    this.P_context.setPlayerState(Context.walkDownState);
                }
            }
        }
    });

    window.StandDownState = StandDownState;
}());
View Code

Context

            walkUp: function () {
                this._state.walkUp();
            },
            walkDown: function () {
                this._state.walkDown();
            },
...
        Static: {
            walkUpState: new WalkUpState(),
            walkDownState: new WalkDownState(),
...
            standUpState: new StandUpState(),
            standDownState: new StandDownState()
        }

解决问题

解决“drawImage中的dx、dy和clearRect中的x、y按比例缩放

如今我须要解决在第3篇博文中提到的问题

问题描述

若是把PlayerSprite.js -> draw -> drawImage:

context.drawImage(this.currentAnim.getImg(), frame.x, frame.y, frame.width, frame.height, this.x, this.y, frame.imgWidth, frame.imgHeight);

中的this.x、this.y设定成260、120:

context.drawImage(this.currentAnim.getImg(), frame.x, frame.y, frame.width, frame.height, 260, 120, frame.imgWidth, frame.imgHeight);

则无论画布canvas的width、height如何设置,玩家人物都固定在画布的右下角!!!

照理说,坐标应该为一个固定值,不该该随画布的变化而变化。即若是canvas.width = 300, drawImage的dx=300,则图片应该在画布右侧边界处;若是canvas.width 变为600,则图片应该在画布中间!而不该该还在画布右侧边界处!

问题分析

这是由于我在PlayerLayer的建立canvas时,使用了css设置画布的大小,所以致使了画布按比例缩放的问题。

PlayerLayer

P__createCanvas: function () {
    var canvas = $("<canvas/>", {
        //id: id,
        width: bomberConfig.canvas.WIDTH.toString(),
        height: bomberConfig.canvas.HEIGHT.toString(),
        css: {
            "position": "absolute",
            "top": bomberConfig.canvas.TOP,
            "left": bomberConfig.canvas.LEFT,
            "border": "1px solid red",
            "z-index": 1
        }
    });
    $("body").append(canvas);

    this.P__canvas = canvas[0];
}

详见关于使用Css设置Canvas画布大小的问题

解决方案

经过HTML建立canvas,并在Html中设置它的width和height:

<canvas width="500" height="500">
</canvas>

本文最终领域模型

查看大图

高层划分

新增包

  • 事件管理包
    KeyState、KeyEventManager

分析

状态类应该放到哪一个包?

状态类与玩家精灵类PlayerSprite互相依赖且共同重用,所以应该都放到“精灵”这个包中。

本文层、包

对应领域模型

  • 辅助操做层
    • 控件包
      PreLoadImg
    • 配置包
      Config
  • 用户交互层
    • 入口包
      Main
  • 业务逻辑层
    • 辅助逻辑
      • 工厂包
        BitmapFactory、LayerFactory、SpriteFactory
      • 事件管理包
        KeyState、KeyEventManager
    • 游戏主逻辑
      • 主逻辑包
        Game
    • 层管理
      • 层管理实现包
        PlayerLayerManager、MapLayerManager
      • 层管理抽象包
      • LayerManager
      • 层实现包
        PlayerLayer、MapLayer
      • 层抽象包
        Layer
      • 集合包
        Collection
    • 精灵
      • 精灵包
        PlayerSprite、Context、PlayerState、WalkLeftState、WalkRightState、WalkUpState、WalkDownState、StandLeftState、StandRightState、StandUpState、StandDownState
      • 动画包
        Animation、GetSpriteData、SpriteData、GetFrames、FrameData
  • 数据操做层
    • 地图数据操做包
      MapDataOperate
    • 路径数据操做包
      GetPath
    • 图片数据操做包
      Bitmap
  • 数据层
    • 地图包
      MapData
    • 图片路径包
      ImgPathData

本文参考资料

HTML5超级玛丽小游戏源代码

彻底分享,共同进步——我开发的第一款HTML5游戏《驴子跳》

欢迎浏览上一篇博文:炸弹人游戏开发系列(4):炸弹人显示与移动

欢迎浏览下一篇博文:炸弹人游戏开发系列(6):实现碰撞检测,设置移动步长

相关文章
相关标签/搜索