HTML5 canvas 實現回合制戰棋遊戲(1):加載和繪製圖形

遊戲介紹

之前寫了一個 python 版本的回合制戰棋遊戲,最近學習了一下 javascript ES6 語法,因爲 ES6 新增加了 class 語法糖,可以比較方便的將 python 的 class 實現移植過來。使用 html5 canvs 繪製遊戲圖像,利用 javascript ES6 重新實現了這個遊戲。

python 戰棋遊戲代碼實現

遊戲實現了類似英雄無敵3 中戰鬥場景的回合制玩法:

  • 對戰雙方每個生物每一輪有一次行動機會,可以行走或攻擊對方。
  • 每個生物屬性有:行走範圍,速度,生命,傷害,防禦,攻擊 和 是否是遠程兵種。
  • 當把對方生物都消滅時,即勝利。
  • 實現了簡單的AI。

遊戲截圖如下:

圖1demo1圖1中,是遊戲的開始頁面,‘start game’ 是一個按鈕,點擊開始運行遊戲。

圖2demo2圖2中,目前輪到行動的生物是我方的左下角背景爲淺藍色的步兵,可以看到背景爲深藍色的方格爲步兵可以行走的範圍。背景爲綠色的方格爲目前選定要行走到得方格。鼠標指向敵方生物,如果敵方生物背景方格顏色變成黃色,表示可以攻擊,可以看到允許攻擊斜對角的敵人。圖中還有石塊,表示不能移動到的地圖方格。

完整代碼

遊戲實現代碼的 github 鏈接 戰棋遊戲
這邊是 csdn 的下載鏈接 戰棋遊戲

代碼目錄

爲了更好的管理,將遊戲的資源,配置文件和代碼分成了多個目錄進行保存。

code
介紹下代碼各個目錄的使用:

  • images 目錄:存放遊戲中用到的生物和地圖格子圖片。
  • data 目錄:存放遊戲會用到的配置文件,保存關卡地圖配置的 entity_data.js,保存生物屬性配置的 entity.js
  • js 目錄:存放遊戲實現的 js 文件。
  • index.html:在瀏覽器中打開這個 html 文件來運行遊戲。

遊戲運行

1.支持的瀏覽器

  • 目前有在 Firefox, Google Chrome 和 Microsoft Edge 上測試 ok。
  • Mac Safari 沒有測試過,IE 是不支持 Javascript ES6 語法的。

2.運行

直接用瀏覽器執行代碼根目錄下的 index.html 文件。

HTML5 canvas 繪製圖形

canvas 介紹

canvas 使用 JavaScript 在網頁上繪製圖像。canvas 畫布是一個矩形區域,可以在這個矩形上以像素爲單位繪製各種圖形(圖片,文字,矩形等)。canvas 提供了繪製線段、矩形、圓形、文字和圖像的方法。

繪製圖形前,需要先創建一個 canvas 對象,設置 canvas 矩形區域的寬度和高度,調用 getContext 函數返回一個對象 ctx,然後就可以用 ctx 對象來繪製圖形。

var canvas = document.createElement("canvas");
var ctx = canvas.getContext("2d");
canvas.width = MAP_WIDTH;
canvas.height = MAP_HEIGHT;
document.body.appendChild(canvas);

繪製函數

在一個遊戲中需要繪製圖片,文字和圖形。圖形可以有線段,矩形,複雜點的有多邊形,橢圓等。

HTML5 canvas 提供了函數來繪製線段、矩形、字符和圖像,可以通過下面鏈接看下函數使用介紹:

HTML 5 Canvas 參考手冊

但是 canvas 提供的繪製功能不是簡單的一個函數就能實現,需要設置各種參數,下邊是遊戲中提供的封裝函數,代碼在 js/tool.js 文件中:

  • 繪製線段函數 drawLine : 繪製一條從起點(start_x, start_y) 到終點(end_x, end_y)的線段,顏色爲 color。
  • 繪製圖片函數 drawImage :source_rect 表示截取圖片資源上的一個矩形大小的圖形,繪製在 dest_rect 表示的 canvas 上的矩形中。
  • 繪製四邊形函數 drawRect:調用的 fillRect 函數繪製一個“被填充”的矩形。繪製一個左上角位置在(x,y),寬度爲 width,高度爲 height 的矩形,矩形填充的顏色爲 color。
  • 繪製字符函數 drawText:調用的 fillText 函數繪製一行“被填充的”文本。font 表示字符的大小,字體等設置。
function drawLine(ctx, color, start_x, start_y, end_x, end_y) {
    ctx.strokeStyle = color;
    ctx.beginPath();
    ctx.moveTo(start_x, start_y);
    ctx.lineTo(end_x, end_y);
    ctx.stroke();
}

function drawImage(ctx, img, source_rect, dest_rect) {
    ctx.drawImage(img, source_rect[0], source_rect[1], source_rect[2], source_rect[3],
                  dest_rect[0], dest_rect[1], dest_rect[2], dest_rect[3]);
}

function drawRect(ctx, color, x, y, width, height) {
    ctx.fillStyle = color;
    ctx.fillRect(x, y, width, height);
}

function drawText(ctx, color, str, font, x, y) {
    ctx.font = font;
    ctx.fillStyle = color;
    ctx.fillText(str, x, y);
}

加載圖片

在遊戲中每個生物都有一個圖片,需要先將圖片加載完成後,才能在 canvas 中繪製,不然瀏覽器執行時會報錯。一個圖片對象可以在 canvas 上繪製多個相同類型的生物,所以我們可以用一個 Map 對象來預先加載所有的圖片對象,然後在創建具體的類時(比如生物類),獲取這個圖片對象。

所有圖片的名稱和資源位置定義在 js\contants.py 文件中,IMAGE_SRC_MAP Map 對象保存了圖片名稱和資源位置的對應關係。

// IMAGE NAME
const GRID_IMAGE = 'tile.png';
const DEVIL = 'devil';
const FOOTMAN = 'footman';
const MAGICIAN = 'magician';
const EVILWIZARD = 'evilwizard';
const FIREBALL = 'fireball';

var IMAGE_SRC_MAP = new Map([
    [GRID_IMAGE, 'images/tile.png'],
    [DEVIL,      'images/devil.png'],
    [FOOTMAN,    'images/footman.png'],
    [MAGICIAN,   'images/magician.png'],
    [EVILWIZARD, 'images/evilwizard.png'],
    [FIREBALL,   'images/fireball.png']
]);

加載圖片的代碼在 js/tool.js 文件中:

  • loadAllGraphics 函數:遍歷參數 img_src_map Map 對象,每個圖片創建一個 對象,對象的成員 img 是一個 Image 對象,成員 ready 表示圖片是否加載完成。將這個對象保存到 IMAGE_MAP Map 對象中。
  • getMapGridImage 函數:因爲一個圖片中含有多個地圖背景的小圖片。將每個小圖片都創建一個 ImageWrapper 封裝類來表示。
function loadAllGraphics(img_src_map) {
    for(let key of img_src_map.keys()) {
        let tmp = {'img':new Image(), 'ready':false};
        IMAGE_MAP.set(key, tmp);
        tmp.img.onload = function() {
            let tmp = IMAGE_MAP.get(key);
            tmp.ready = true;
        };
        tmp.img.src = img_src_map.get(key);
    }
}

function getMapGridImage() {
    let grid_rect = new Map([
        [MAP_STONE.toString(), [0, 21, 20, 20]],
        [MAP_GRASS.toString(), [0, 0, 20, 20]]
    ]);
    let grid_image_map = new Map();

    for(let key of grid_rect.keys()) {
        let img = new ImageWrapper(GRID_IMAGE, grid_rect.get(key));
        grid_image_map.set(key, img);
    }
    return grid_image_map;
}

function getLevelData(level_num) {
    let level = 'level_' + level_num;
    return LEVEL_MAP.get(level);
}

var IMAGE_MAP = new Map();
loadAllGraphics(IMAGE_SRC_MAP);

var GRID_IMAGE_MAP = getMapGridImage();

js\contants.py 文件中提供了一個圖片的封裝類,

  • 構造函數 constructor :從 IMAGE_MAP Map 對象中根據圖片名稱獲取圖片對象,rect 表示截取圖片資源上的一個矩形大小的圖形。
  • draw 函數:根據 ready 成員變量值判斷該圖片是否加載完成,這樣外部調用者可以不用考慮圖片是否加載完成的細節問題。
class ImageWrapper{
    constructor(name, rect) {
        this.img = IMAGE_MAP.get(name);
        this.rect = rect;
    }
    
    draw(ctx, dest_rect) {
        if(this.img.ready) {
            drawImage(ctx, this.img.img, this.rect, dest_rect);
        }
    }
}

看下圖片封裝類的實際應用,在 js\entity.py 文件中實現了遠程生物的火球類,在 FireBall 類的構造函數中,調用 loadImage 函數創建了一個 ImageWrapper 類對象,這樣在 draw 函數中繪製火球圖片時,可以不用考慮圖片是否加載完成的問題。

class FireBall{
    constructor(x, y, enemy, hurt) {
        this.loadImage();
        this.pos = {'x':x, 'y':y};
		...
    }
    
    loadImage() {
        let rect = [0, 0, 14, 14];
        this.img = new ImageWrapper(FIREBALL, rect);
    }

    getRect() {
        return [this.pos.x - 7, this.pos.y - 7, 14, 14];
    }
      
    draw(ctx) {
        this.img.draw(ctx, this.getRect());
    }
}

生物行走動畫繪製

動畫顯示

生物在空閒,行走和攻擊狀態時,在遊戲中一般會有一個動畫效果的顯示,本遊戲中只實現行走狀態的動畫效果,方法很簡單。利用生物的兩個相似的圖形,按照一定的時間間隔來循環顯示這兩個圖形,就可以展示出生物行走的動畫效果。

js\entity.py 文件中生物 Entity 類中,下面幾個函數實現了行走動畫效果,省略了不相關的代碼:

  • constructor 構造函數:imgs 數組保存圖片對象,img_index 當前顯示的圖片對象在 imgs 數組中的索引值, img 表示當前顯示的圖片對象。
  • loadImages 函數:用來加載生物的圖形,因爲要實現生物行走的動畫效果,所以要加載多個圖形。先看下示例的生物圖片,圖片路徑是 images/footman.png。可以看到這個圖片上有八個小的生物圖形,我們目前只需要其中右上方的兩個小圖形。rect_list 數組保存了右上方兩個小圖形在圖片中的位置和大小,用來創建兩個 ImageWrapper 圖片對象。
    footman
  • update 函數:生物的更新函數,在遊戲的每個循環中都會被調用。current_time 是遊戲當前的時間值,單位是毫秒,animate_timer 是上一次圖片切換的時間值,可以看到每隔 200 毫秒,就會修改圖片的索引值 img_index,實現生物的行走動畫效果。
  • draw 函數:生物的繪製函數,繪製生物當前的圖片對象。
class Entity{
	constructor(group, name, map_x, map_y, data) {
        ...
        this.imgs = [];
        this.img_index = 0;
        this.loadImages(name);
        this.img = this.imgs[this.img_index];
        ...
    }
    
    loadImages(name) {
        let rect_list = [[64, 0, 32, 32], [96, 0, 32, 32]];
        for(let i in rect_list) {
            this.imgs.push(new ImageWrapper(name, rect_list[i]));
        }
    }
    
    update(current_time, ctx, level) {
    	this.current_time = current_time;
    	if(this.state == WALK) {
            if((this.current_time - this.animate_timer) > 200) {
                if(this.img_index == 0) {
                    this.img_index = 1;
                }
                else {
                    this.img_index = 0;
                }
                this.animate_timer =  this.current_time;
            }
            ...
        }
        ...
    }
    
    draw(ctx) {
        this.img = this.imgs[this.img_index];
        this.img.draw(ctx, this.getRect());
        ...
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章