炸彈人游戲開發系列(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/>", {
                    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來實現狀態的邏輯和狀態切換。

那為什么我要用狀態模式了?

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,
        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):實現碰撞檢測,設置移動步長 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM