前言

上一篇博文讲了如何造一条蛇,现在蛇有了,要让它自由的活动起来,就得有个地图啊,而且只能走也不行呀,还得有点吃的,所以还得加点食物,这一篇博文就来讲讲如何添加地图和食物。

预览效果

当前项目最新效果:http://whxaxes.github.io/slither/ (由于代码一直在更新,效果可能会比本文所述的更多)

功能分析

slither.io的地图是类似于rpg游戏的大地图,所以,我们需要两个新的类,一个是地图类:Map,一个是视窗类:Frame,地图类就是整个大地图的抽象,视窗类就是可视界面的抽象。

而怎么做成蛇动的时候,绘制位置不动,而是地图动呢。其实原理也很简单,如果看过上一篇文章的读者,应该还记得Base类里有两个参数:paintX以及paintY,这两个是绘制坐标,跟蛇的坐标不同的就是,绘制坐标是蛇的实际坐标减去视窗的坐标。

  get paintX() {
return this.x - frame.x;
} get paintY() {
return this.y - frame.y;
}

每次render的时候,绘制的坐标就是用的这两个参数,同时适当的调整一下视窗的坐标,就可以做成相对于视窗中蛇没移动,但是看上去蛇移动了的效果。

Base类里还有一个参数叫visible:

/**
* 在视窗内是否可见
* @returns {boolean}
*/
get visible() {
const paintX = this.paintX;
const paintY = this.paintY;
const halfWidth = this.width / 2;
const halfHeight = this.height / 2; return (paintX + halfWidth > 0)
&& (paintX - halfWidth < frame.width)
&& (paintY + halfHeight > 0)
&& (paintY - halfHeight < frame.height);
}

用于判断实例在视窗frame中是否可见,如果不可见,就不需要调用绘制接口了,从而提升游戏性能。

而食物类就比较简单了,继承Base类后,在地图中随机出一定数量,再进行一下蛇头与食物的碰撞检测即可。

接着再细讲一下各个类的实现。

视窗类

因为地图类也依赖视窗类,所以先看视窗类。代码量相当少,所以直接全部贴出:

// 视窗类
class Frame {
init(options) {
this.x = options.x;
this.y = options.y;
this.width = options.width;
this.height = options.height;
} /**
* 跟踪某个对象
*/
track(obj) {
this.translate(
obj.x - this.x - this.width / 2,
obj.y - this.y - this.height / 2
);
} /**
* 移动视窗
* @param x
* @param y
*/
translate(x, y) {
this.x += x;
this.y += y;
}
} export default new Frame();

由于视窗在整个游戏中只有一个,所以做成了单例的。视窗类就只有几个属性,x坐标,y坐标,宽度和高度。x坐标和y坐标是相对于地图左上角的值,width和height一般就是canvas的大小。

track方法是跟踪某个对象,也就是视窗跟着对象的移动而移动。在main.js中调用跟踪蛇类:

// 让视窗跟随蛇的位置更改而更改
frame.track(snake);

地图类

地图类跟视窗类一样也是整个游戏里只有一个,所以也做成单例的,而且由于,整个游戏的元素,都是基于地图上的,所以我也把canvas的2d绘图对象挂载到了地图类上。先看地图类的部分代码:

  constructor() {
// 背景块的大小
this.block_w = 150;
this.block_h = 150;
} /**
* 初始化map对象
* @param options
*/
init(options) {
this.canvas = options.canvas;
this.ctx = this.canvas.getContext('2d'); // 地图大小
this.width = options.width;
this.height = options.height;
} /**
* 清空地图上的内容
*/
clear() {
this.ctx.clearRect(0, 0, frame.width, frame.height);
}

构造函数中,定义一下地图背景的方格块的大小,然后就是init方法,给外部初始化用的,因为地图的位置是固定的,所以不需要坐标值,只需要宽度和高度即可。clear是给外部调用用来清除画布。

再看地图类的渲染方法:

  /**
* 渲染地图
*/
render() {
const beginX = (frame.x < 0) ? -frame.x : (-frame.x % this.block_w);
const beginY = (frame.y < 0) ? -frame.y : (-frame.y % this.block_h);
const endX = (frame.x + frame.width > this.width)
? (this.width - frame.x)
: (beginX + frame.width + this.block_w);
const endY = (frame.y + frame.height > this.height)
? (this.height - frame.y)
: (beginY + frame.height + this.block_h); // 铺底色
this.ctx.fillStyle = '#999';
this.ctx.fillRect(beginX, beginY, endX - beginX, endY - beginY); // 画方格砖
this.ctx.strokeStyle = '#fff';
for (let x = beginX; x <= endX; x += this.block_w) {
for (let y = beginY; y <= endY; y += this.block_w) {
const cx = endX - x;
const cy = endY - y;
const w = cx < this.block_w ? cx : this.block_w;
const h = cy < this.block_h ? cy : this.block_h; this.ctx.strokeRect(x, y, w, h);
}
}
}

其实就是根据视窗的位置,来进行局部绘制,如果进行整个地图的绘制,会超级消耗性能。所以只绘制需要展示的那一块。

按照slither.io的功能,大地图有了,还得画个小地图:

  /**
* 画小地图
*/
renderSmallMap() {
// 小地图外壳, 圆圈
const margin = 30;
const smapr = 50;
const smapx = frame.width - smapr - margin;
const smapy = frame.height - smapr - margin; // 地图在小地图中的位置和大小
const smrect = 50;
const smrectw = this.width > this.height ? smrect : (this.width * smrect / this.height);
const smrecth = this.width > this.height ? (this.height * smrect / this.width) : smrect;
const smrectx = smapx - smrectw / 2;
const smrecty = smapy - smrecth / 2; // 相对比例
const radio = smrectw / this.width; // 视窗在小地图中的位置和大小
const smframex = frame.x * radio + smrectx;
const smframey = frame.y * radio + smrecty;
const smframew = frame.width * radio;
const smframeh = frame.height * radio; this.ctx.save();
this.ctx.globalAlpha = 0.8; // 画个圈先
this.ctx.beginPath();
this.ctx.arc(smapx, smapy, smapr, 0, Math.PI * 2);
this.ctx.fillStyle = '#000';
this.ctx.fill();
this.ctx.stroke(); // 画缩小版地图
this.ctx.fillStyle = '#999';
this.ctx.fillRect(smrectx, smrecty, smrectw, smrecth); // 画视窗
this.ctx.strokeRect(smframex, smframey, smframew, smframeh); // 画蛇蛇位置
this.ctx.fillStyle = '#f00';
this.ctx.fillRect(smframex + smframew / 2 - 1, smframey + smframeh / 2 - 1, 2, 2); this.ctx.restore();
}

这个也没什么难度,就是叠图层而已。不再解释

最后再export出去:export default new Map();即可。

组合

在main.js中,直接初始化一下:

// 初始化地图对象
map.init({
canvas,
width: 5000,
height: 5000
}); // 初始化视窗对象
frame.init({
x: 1000,
y: 1000,
width: canvas.width,
height: canvas.height
});

然后在动画循环中,让视窗跟随蛇的实例snake,然后再进行相应的render即可,render的顺序关系到元素的层级,所以小地图是最后才render :

// 让视窗跟随蛇的位置更改而更改
frame.track(snake); map.render(); snake.render(); map.renderSmallMap();

食物类

再讲一下食物类,也是非常的简单,直接继承Base类,然后做个简单的发光动画效果即可,代码量不多,也全部贴出:

export default class Food extends Base {
constructor(options) {
super(options); this.point = options.point;
this.r = this.width / 2; // 食物的半径, 发光半径
this.cr = this.width / 2; // 食物实体半径
this.lightDirection = true; // 发光动画方向
} update() {
const lightSpeed = 1; this.r += this.lightDirection ? lightSpeed : -lightSpeed; // 当发光圈到达一定值再缩小
if (this.r > this.cr * 2 || this.r < this.cr) {
this.lightDirection = !this.lightDirection;
}
} render() {
this.update(); if (!this.visible) {
return;
} map.ctx.fillStyle = '#fff'; // 绘制光圈
map.ctx.globalAlpha = 0.2;
map.ctx.beginPath();
map.ctx.arc(this.paintX, this.paintY, this.r, 0, Math.PI * 2);
map.ctx.fill(); // 绘制实体
map.ctx.globalAlpha = 1;
map.ctx.beginPath();
map.ctx.arc(this.paintX, this.paintY, this.cr, 0, Math.PI * 2);
map.ctx.fill();
}
}

然后在main.js中,进行食物生成:

// 食物生成方法
const foodsNum = 100;
const foods = [];
function createFood(num) {
for (let i = 0; i < num; i++) {
const point = ~~(Math.random() * 30 + 50);
const size = ~~(point / 3); foods.push(new Food({
x: ~~(Math.random() * (map.width + size) - 2 * size),
y: ~~(Math.random() * (map.height + size) - 2 * size),
size, point
}));
}
}

然后在动画循环中进行循环并且渲染即可:

// 渲染食物, 以及检测食物与蛇头的碰撞
foods.slice(0).forEach(food => {
food.render(); if (food.visible && collision(snake.header, food)) {
foods.splice(foods.indexOf(food), 1);
snake.eat(food);
createFood(1);
}
});

渲染的同时,也跟蛇头进行一下碰撞检测,如果产生了碰撞,则从食物列表中删掉吃掉的实物,并且调用蛇类的eat方法,然后再随机生成一个食物补充。

因为食物是圆,蛇头也是圆,所以碰撞检测就很简单了:

/**
* 碰撞检测
* @param dom
* @param dom2
* @param isRect 是否为矩形
*/
function collision(dom, dom2, isRect) {
const disX = dom.x - dom2.x;
const disY = dom.y - dom2.y; if (isRect) {
return Math.abs(disX) < (dom.width + dom2.width)
&& Math.abs(disY) < (dom.height + dom2.height);
} return Math.hypot(disX, disY) < (dom.width + dom2.width) / 2;
}

然后再看一下蛇的eat方法:

/**
* 吃掉食物
* @param food
*/
eat(food) {
this.point += food.point; // 增加分数引起虫子体积增大
const newSize = this.header.width + food.point / 50;
this.header.setSize(newSize);
this.bodys.forEach(body => {
body.setSize(newSize);
}); // 同时每吃一个食物, 都增加身躯
const lastBody = this.bodys[this.bodys.length - 1];
this.bodys.push(new SnakeBody({
x: lastBody.x,
y: lastBody.y,
size: lastBody.width,
color: lastBody.color,
tracer: lastBody
}));
}

调用该方法后,会使蛇的分数增加,同时增加体积,以及身躯长度。

至此,地图以及食物都做好了。

照例贴出github地址:https://github.com/whxaxes/slither

仿造slither.io第二步:加个地图,加点吃的的更多相关文章

  1. 仿造slither.io第一步:先画条蛇

    前言 最近 slither.io 貌似特别火,中午的时候,同事们都在玩,包括我自己也是玩的不亦乐乎. 好久好久没折腾过canvas相关的我也是觉得是时候再折腾一番啦,所以就试着仿造一下吧.楼主也没写过 ...

  2. 百度地图-省市县联动加载地图 分类: Demo JavaScript 2015-04-26 13:08 530人阅读 评论(0) 收藏

    在平常项目中,我们会遇到这样的业务场景: 客户希望把自己的门店绘制在百度地图上,通过省.市.区的选择,然后加载不同区域下的店铺位置. 先看看效果图吧: 实现思路: 第一步:整理行政区域表: 要实现通过 ...

  3. 带你剖析WebGis的世界奥秘----瓦片式加载地图(转)

    带你剖析WebGis的世界奥秘----瓦片式加载地图 转:https://zxhtom.oschina.io/zxh/20160805.html  编程  java  2016/08/05 0留言,  ...

  4. 解决ArcGIS API for Silverlight 加载地图的内外网访问问题

    原文:解决ArcGIS API for Silverlight 加载地图的内外网访问问题 先上一个类,如下: public class BaseClass { public static string ...

  5. MapXtreme在asp.net中的使用之加载地图(转)

    MapXtreme在asp.net中的使用之加载地图(转) Posted on 2010-05-04 19:44 Happy Coding 阅读(669) 评论(0) 编辑 收藏 1.地图保存在本地的 ...

  6. ArcGis API for JavaScript学习——加载地图

    ArcGis API for JavaScript开发笔记——加载地图 在这个例子中使用的离线部署的API(请参见 http://note.youdao.com/noteshare?id=f42865 ...

  7. cocos2d-x游戏开发系列教程-坦克大战游戏加载地图的编写

    上节课写了关卡选择场景,那么接下来写关卡内容,先写最基本的地图的加载 我们新建一个场景类,如下所示: class CityScene : public cocos2d::CCLayer { publi ...

  8. arcgis api for javascript本地部署加载地图

    最近开始学习arcgis api for javascript,发现一头雾水,决定记录下自己的学习过程. 一.下载arcgis api for js 4.2的library和jdk,具体安装包可以去官 ...

  9. 带你剖析WebGis的世界奥秘----瓦片式加载地图

    WebGIS应用程序的页面能够通过HTML.JSP.ASP或任何任何类型的Web页文件构成,其特殊之处在于,它的请求提交的方法并不是通过常用的 "超链接"形式,而是使用鼠标与Web ...

随机推荐

  1. Windows服务调试小结(附Demo)

    本文版权归mephisto和博客园共有,欢迎转载,但须保留此段声明,并给出原文链接,谢谢合作. 阅读目录 介绍 搭建环境 调试方式 Demo下载 本文版权归mephisto和博客园共有,欢迎转载,但须 ...

  2. 009.CentOS 6.7安装运行netmap

    一.netmap简介: 1.netmap是一个高性能收发原始数据包的框架,由Luigi Rizzo等人开发完成,其包含了内核模块以及用户态库函数.其目标是,不修改现有操作系统软件以及不需要特殊硬件支持 ...

  3. 【JAVA 小结】Java关于类与对象的代码

    分别建立2个类class works 和 Person import java.io.*; public class works { public static void main(String[] ...

  4. Linux Gitlab

    一.简介 GitLab是利用 Ruby on Rails 一个开源的版本管理系统,实现一个自托管的Git项目仓库,可通过Web界面进行访问公开的或者私人项目.它拥有与Github类似的功能,能够浏览源 ...

  5. CURL使用方法详解

    php采集神器CURL使用方法详解 作者:佚名  更新时间:2016-10-21   对于做过数据采集的人来说,cURL一定不会陌生.虽然在PHP中有file_get_contents函数可以获取远程 ...

  6. 调试2440 RAM拷贝至SDRAM遇到的问题

    汇编代码主要是初始化一些寄存器,关狗,初始化时钟,初始化存储管理器以便访问内存,然后将SoC上4k RAM数据拷贝至SDRAM,然后在SRAM里面运行,由于代码未正常跑起来,于是使用JLinkExe来 ...

  7. [译] 企业级 OpenStack 的六大需求(第 1 部分):API 高可用、管理和安全

    全文包括三部分: 第一部分:API 高可用和管理以及安全模型 第二部分:开放架构和混合云兼容 第三部分:弹性架构和全球交付 引言 OpenStack 是构造企业级私有云的非常理想的基础.它立志成为新一 ...

  8. MMORPG大型游戏设计与开发(part2 of net)

    网络第二部分的将要给大家描述的是网络代码方面的设计,从基础的代码讲起,了解详细的网络模块构架. 没有放出整个源代码,是因为其中还有许多不足的地方,不过想必大家应该也能猜想出这个项目源码的地址了.不过对 ...

  9. Stanford机器学习笔记-6. 学习模型的评估和选择

    6. 学习模型的评估与选择 Content 6. 学习模型的评估与选择 6.1 如何调试学习算法 6.2 评估假设函数(Evaluating a hypothesis) 6.3 模型选择与训练/验证/ ...

  10. AC小笔记

    1:基本库函数的使用 Rand()函数,可以产生0~32767之间的随机数. a+rand()%(b-a)  可以得到 [a,b] 之间的随机数. 2:基本数据类型的使用 可以使用强制类型转换 例如: ...