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


    前言

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

    本文目的

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

    完善炸弹人移动,增加上下方向的移动

    本文主要内容

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

    开发策略

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

    性能优化

    双缓冲

    什么是双缓冲

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

    为什么要用双缓冲

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

    那么应该如何优化呢?

    • 每次只绘制地图中变化的部分。
    • 当变化的范围也很大时(涉及到多个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/>", {
                         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/>", {
                         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来实现状态的逻辑和状态切换。

    那为什么我要用状态模式了?

    1、做这个游戏是为了学习,状态模式我之前没有实际应用过,因此可以在此处练手

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

    3、扩展方便。目前实现了炸弹人左右移动,后面还会实现炸弹人上下移动。如果用状态模式的话,只需要增加四个状态:上走、上站、下走、下站,再对应修改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,
             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):实现碰撞检测,设置移动步长 

  • 相关阅读:
    数组塌陷现象
    深浅拷贝的区别
    冒泡排序,选择排序的应用
    JavaScript双重循环的嵌套
    Css Grid网格布局
    css3动画详细介绍
    Python tkinter Label Widget relief upload image
    XXXFD
    XXX1
    Python爬取一个简单网页的HTML代码
  • 原文地址:https://www.cnblogs.com/chaogex/p/3265119.html
Copyright © 2020-2023  润新知