小言_互联网的博客

HTML5 canvas 实现回合制战棋游戏(1):加载和绘制图形

297人阅读  评论(0)

游戏介绍

之前写了一个 python 版本的回合制战棋游戏,最近学习了一下 javascript ES6 语法,因为 ES6 新增加了 class 语法糖,可以比较方便的将 python 的 class 实现移植过来。使用 html5 canvs 绘制游戏图像,利用 javascript ES6 重新实现了这个游戏。

python 战棋游戏代码实现

游戏实现了类似英雄无敌3 中战斗场景的回合制玩法:

  • 对战双方每个生物每一轮有一次行动机会,可以行走或攻击对方。
  • 每个生物属性有:行走范围,速度,生命,伤害,防御,攻击 和 是否是远程兵种。
  • 当把对方生物都消灭时,即胜利。
  • 实现了简单的AI。

游戏截图如下:

图1图1中,是游戏的开始页面,‘start game’ 是一个按钮,点击开始运行游戏。

图2图2中,目前轮到行动的生物是我方的左下角背景为浅蓝色的步兵,可以看到背景为深蓝色的方格为步兵可以行走的范围。背景为绿色的方格为目前选定要行走到得方格。鼠标指向敌方生物,如果敌方生物背景方格颜色变成黄色,表示可以攻击,可以看到允许攻击斜对角的敌人。图中还有石块,表示不能移动到的地图方格。

完整代码

游戏实现代码的 github 链接 战棋游戏
这边是 csdn 的下载链接 战棋游戏

代码目录

为了更好的管理,将游戏的资源,配置文件和代码分成了多个目录进行保存。


介绍下代码各个目录的使用:

  • 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 图片对象。
  • 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());
        ...
    }
}

转载:https://blog.csdn.net/marble_xu/article/details/104568939
查看评论
* 以上用户言论只代表其个人观点,不代表本网站的观点或立场