炸彈人遊戲開發系列(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):實現碰撞檢測,設置移動步長

相關文章
相關標籤/搜索