前言
很早之前,我就关注到了Github上一款叫做shapez的高星游戏,主要玩法就和他的名字一样,是个需要利用开采器,传送带等各种工具来完成每个关卡所需要收集的开采物的游戏(游戏链接:shapez.io/ , 原创作者:github.com/tobspr-game… )。边玩的时候一边感叹,怎么能用JS做出性能这么优秀的高难度逻辑的游戏。那时候的我对JS的运用只能说是停留在能操控DOM元素。做点简单逻辑游戏的份上。也幻想尝试着做这个游戏,但是直接就在创建地图的阶段就暴毙了。
往后的时间里,我一直都在不断磨炼自己的JS运用技术,特意去参考学习了很多大佬解析的一些游戏源代码。相对的在这方面的提升也是肉眼可见的。
到了今天的10月中旬慢慢终于闲下来的时候,我终于是想起了shapez这款游戏,决心一定要挑战一下哪怕只是抽象的能完成多少就完成多少。
分析
原版游戏分析
先对原版游戏进行一下分析,他的地图是可以不断动态创建的,且进行缩小到最小地图之后大概显示50万个网格,但是经过研究了一段时间后发现,在不放置任何工具的时候他永远只会不断更新一块4块16*16大小的网格高亮区域。其余地方是不会以高刷新率重新更新全部画布的,但如果在其他区域放置一个工具的话,会在那个放置点的16 * 16的方向上形成一个新的高亮刷新区域。
那么对于地图上那些蓝的绿的红的开采区是如何生成的我后面也慢慢想到一个叫做柏林噪点的东西,虽然不能百分百确定,但至少我觉得生成的东西就和这游戏地图上的板块是非常相似的
其余的工具放置之类的功能相对就比较简单这里就不做更多讲解了
制作流程
这是我对自己要所要完成的功能做的一个最基本的规划,规划内容如下:
graph TD
canvas网格地图绘制 --> 地图缩放拖动动态创建显示 --> 地图板块填充噪点等 --> 地图工具放置 --> 地图开采运行/中心接收/进入下一关
制作
那么分析完了就可以开始动手了,但是为了方便以后的制作,我对自己的代码规范,以及预留一些添加后面功能板块的位置也是很有必要的。
canvas网格地图绘制
先要想想这个网格怎么创建,根据刚刚的分析如果光是创建2级数组作为地图存储的话会导致后面要完成放置工具变成高亮刷新区域后期就很难添加了,所以我的想法是这样的。 地图X Y坐标分别代表分别是二维数组的maps[y][x],在数组值里保存一个Class类,里面再存一个二维数组:如下面的。(ps:应该是还有别的方法的但是我觉得最简单的就是这种)
[
[
{grids: [[]]},{},{},{}
],
[{},{},{},{}],
[{},{},{},{}],
]
然后就是封装类开始实现,先封装好canvas相关操作的类
// 绘制canvas地图
class Map {
constructor() {
this.canvas = document.querySelector('canvas');
this.ctx = this.canvas.getContext('2d');
// 屏幕大小一致的canvas
window.onresize = () => {
this.canvas.width = document.documentElement.clientWidth;
this.canvas.height = document.documentElement.clientHeight;
}
// 保存加载好的图片
this.loadImg = {}
this.runTimer = null;
}
// 绘画开始
begin(){
this.ctx.beginPath();
this.ctx.save();
}
// 绘画结束
close(){
this.ctx.closePath();
this.ctx.restore();
}
// 重新绘制地图
one(){
this.ctx.clearRect(0,0, document.documentElement.clientWidth, document.documentElement.clientHeight)
app.maps.flat().map(item => item.update());
}
// 旋转
rotate(x, y, rotate){
this.ctx.translate(x + 3.25, y + 3.25);
this.ctx.rotate(rotate * Math.PI / 180);
this.ctx.translate(-x - 3.25, -y - 3.25);
}
// 绘画图片
drawImage(x, y, name, w = 20, h, cx, cy, cw, ch){
if(this.loadImg[name]){
if(cw && ch){
console.log(cx, cy, cw, ch, x, y, w, h || w);
this.ctx.drawImage(this.loadImg[name], cx, cy, cw, ch, x, y, w, h || w);
}else{
this.ctx.drawImage(this.loadImg[name], x, y, w, h || w);
}
}else{
let img = new Image();
img.src = `${name}`;
img.onload = () => this.loadImg[name] = img;
}
}
}
再去封装一个存入地图数据的Grid类,因为是用Vue写的里面有带app.??? 一般都是之前封装的函数
// 地图里的网格方块
class Item {
constructor(i, j) {
this.i = i;
this.j = j;
this.x = i * app.game.w;
this.y = j * app.game.h;
this.img = '';
this.w = (app.game.w / 16);
}
}
class Grid extends Item{
constructor(i, j, color, prop) {
super(i, j)
// 随机的开采区的颜色
this.color = color;
// 假如是灰色的添加图形
this.prop = prop;
this.main = null;
// 根据show来判断是否随机在二维数组中生成存入一个噪点
this.show = ~~(Math.random() * 3);
this.first = false;
// 存入一个16*16的二维数组
this.grids = new Array(16).fill(0).map((item, j) => {
return new Array(16).fill(0).map((item, i) => {
return new Block(i, j, false, false)
})
})
if(this.show > 0) return;
this.getRandomHP(7, 7, 20)
}
update(){
if(this.show > 0) return;
this.add();
app.map.begin();
this.grids.map((item, j) => {
item.map((item, i) => {
if(app.line){
app.map.ctx.strokeStyle = '#E3E7EA';
app.map.ctx.lineWidth = '0.2'
app.map.ctx.strokeRect(this.x + (i * 6.5), this.y + (j * 6.5), Math.abs(app.game.w) / 16, Math.abs(app.game.h) / 16);
}
})
})
app.map.close();
}
}
初步就绘制出以下的区块
地图缩放拖动
这个拖动功能基本就是计算个偏移量的事情,所以不用多做解释啦。但对于这个定点缩放确实让我想了好一阵子。首先我的想法是通过直接增加绘制方块时候canvas增减绘制 x y 的偏移位和 w h 的大小来完成缩放效果发现并不理想。于是我直接采用了 canvas的 scale 和 translate 方法来进行偏移和缩放。
let ctx = document.getElementById('canvas').getContext('2d');
let obj = {
fontX: 0,
fontY: 0,
fontZoom: 1,
curZoom: 1,
translateX: 0,
translateY: 0,
draw() {
ctx.fillRect(150, 150, 50, 50)
},
zoom(offsetX, offsetY, z) {
ctx.save()
ctx.clearRect(0, 0, 300, 300);
this.curZoom = this.fontZoom + z
this.translateX = offsetX - (offsetX - this.translateX) * this.curZoom / this.fontZoom
this.translateY = offsetY - (offsetY - this.translateY) * this.curZoom / this.fontZoom
ctx.translate(this.translateX, this.translateY);
ctx.scale(this.curZoom, this.curZoom);
this.draw()
ctx.restore()
this.fontY = offsetY
this.fontX = offsetX
this.fontZoom = this.curZoom
}
}
obj.draw()
document.getElementById('canvas').addEventListener('mousewheel', (e) => {
let z = e.deltaY > 0 ? -0.1 : 0.1
obj.zoom(e.offsetX, e.offsetY, z)
})
地图板块填充
噪点生成
那种真的像我的世界地图一样的噪点生成代码理解和写起来实在是麻烦了,所以我只要自创一个符合我要求的也不知道算不算“噪点”的demo。原理很简单,这里我直接用exl表来做一个简单的模型:简单来说就是假如中心点的位置是7,他的四周都是7的以内的随机数。当然为了避免7直接随机到1的尴尬状况,所以我是采用 7/2+(7*Math.random()) 就可以保证得到的数字至少是在一半以上,接下来通过图片就很好理解了
(注意:这些代码只是方便看我单独做的demo,在实际游戏中运用请看在线代码)
class Grid{
constructor() {
this.canvas = document.querySelector('canvas');
this.ctx = this.canvas.getContext('2d');
this.grids = new Array(15).fill(0).map((item, j) => {
return new Array(15).fill(0).map((item, i) => {
return {if: false, rp: false};
})
})
this.getRandomHP(7, 7, 15)
}
//寻找附近的方块
getNearby(i, j, rp){
return [
[i - 1, j],
[i + 1, j],
[i, j - 1],
[i, j + 1],
].map(item => {
// console.log((this.grids[item[1]][item[0]].rp <= 1 && this.grids[item[1]][item[0]].rp !== false), this.grids[item[1]][item[0]].rp)
if(!(this.grids[item[1]] && this.grids[item[1]][item[0]]) || rp <= 1 || this.grids[item[1]][item[0]].if === true) return false;
this.grids[item[1]][item[0]].if = true;
let rand = ~~(Math.random() * rp / 2) + ~~(rp / 2);
return {
i: item[0],
j: item[1],
rp: rand
}
})
}
// 随机生成一个噪点
getRandomHP(i, j, rp){
this.getNearby(i, j, rp).filter(item => {
if(!item) return;
this.getRandomHP(item.i, item.j, item.rp)
})
}
// 根据噪点填充方格
createMap(){
this.grids.map((item, j) => {
item.map((items, i) => {
this.stroke(i, j)
if(items.if) this.draw(i, j)
})
})
}
// 填充方格
draw(i, j){
this.ctx.beginPath();
this.ctx.save()
this.ctx.fillStyle = 'red';
this.ctx.fillRect(i * 30, j * 30, 30, 30);
this.ctx.closePath();
this.ctx.restore()
}
// 划线
stroke(i, j){
this.ctx.beginPath();
this.ctx.save()
this.ctx.strokeStyle = 'red';
this.ctx.strokeRect(i * 30, j * 30, 30, 30);
this.ctx.closePath();
this.ctx.restore()
}
}
let grid = new Grid();
let btn = document.querySelector('button');
btn.onclick = function (){
grid.createMap();
}
实现出来的效果和原版对比
看看 看看,对比一下还是很像的嘛!
各种类型的开采区
开采区就比较简单啦,无非就是把噪点里填充的红色板块填充些别的什么。封装一个通用的开采区的Class类,然后再通过继承来分别创建不同类型的开采区。现在看起来可能有点多余了,但后面是有这几种开采区都需要添加不同的功能的。
// 所有图片网址保存成的对象
let urlImg = {
red: 'https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/bcdf1d95fb2f4203899f56ac4e0d5177~tplv-k3u1fbpfcp-zoom-mark-crop-v2:240:240:0:0.awebp?',
blue: 'https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/a1a8496861a64eb8b72ca6e77a969edb~tplv-k3u1fbpfcp-zoom-mark-crop-v2:240:240:0:0.awebp?',
green: 'https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/0e03f33112ab486b8fbca50797782a78~tplv-k3u1fbpfcp-zoom-mark-crop-v2:240:240:0:0.awebp?',
circular: 'https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/da017de4947847acb217eb51e3a778a5~tplv-k3u1fbpfcp-zoom-mark-crop-v2:240:240:0:0.awebp?',
square: 'https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/ccdd6b9183b745f8914cea5797b677e2~tplv-k3u1fbpfcp-zoom-mark-crop-v2:240:240:0:0.awebp?',
cut: 'https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/f6336eefefa94937adfe7e0ade7d25b1~tplv-k3u1fbpfcp-watermark.image?',
main: 'https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/971af565f1b54f9494614722e50fc6da~tplv-k3u1fbpfcp-zoom-mark-crop-v2:240:240:0:0.awebp?',
weizi: 'https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/0e1b5fdb97a54668914529840c037a6e~tplv-k3u1fbpfcp-zoom-mark-crop-v2:240:240:0:0.awebp?',
miner: 'https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/df7bc18ef96c40af8e8ff74fbed9c5bb~tplv-k3u1fbpfcp-watermark.image?',
remove: 'https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/9934ebad4852405dab42d5e956c4d70f~tplv-k3u1fbpfcp-zoom-mark-crop-v2:460:460:0:0.awebp?',
path_top_bottom: 'https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/9f5ba39cbf6a470b9091164eb1e469f0~tplv-k3u1fbpfcp-watermark.image?',
path_bottom_top: 'https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/89a739cc9118468cbb9d2a70a22bffc2~tplv-k3u1fbpfcp-watermark.image?',
path_left_right: 'https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/e1cbdfa5844d4deb87ac23624e6898e2~tplv-k3u1fbpfcp-watermark.image?',
path_right_left: 'https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/83c6bac94f3b4837a4c5bbdc29fd8979~tplv-k3u1fbpfcp-watermark.image?',
path_left_bottom: 'https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/5416f686ed1a4f80ac940e6ced5459f5~tplv-k3u1fbpfcp-watermark.image?',
path_top_left: 'https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/6274f62a2ffc458f9851e679b96a8134~tplv-k3u1fbpfcp-watermark.image?',
path_right_top: 'https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/24490d7fc5de4c9ea0694bdea8e90f77~tplv-k3u1fbpfcp-watermark.image?',
path_bottom_right: 'https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/148fbcef46e341f19e444fbceabca65c~tplv-k3u1fbpfcp-watermark.image?',
path_right_bottom: 'https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/84bbd233b4d6415f8da8781b91fee3b1~tplv-k3u1fbpfcp-watermark.image?',
path_top_right: 'https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/4d5f15016ac940a28b599e38e43d3636~tplv-k3u1fbpfcp-watermark.image?',
path_left_top: 'https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/6f22d1be13e6424d8274d304a6082ea7~tplv-k3u1fbpfcp-watermark.image?',
path_bottom_left: 'https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/4461257ffd7b4f9fb66f761ef16f0d56~tplv-k3u1fbpfcp-watermark.image?',
}
// 每个方块里的矿物继承Class
class Color extends Item{
constructor(i, j, color, prop = null) {
super(i, j);
this.color = color;
this.prop = prop;
}
update(){
if(app.line){
app.map.ctx.globalAlpha = .5;
app.map.drawImage(this.i + 1.5, this.j + 1.5, urlImg[this.prop || Object.keys(this.color)[0]], Math.abs(app.game.w) / 32);
}
app.map.ctx.fillStyle = Object.values(this.color)[0];
app.map.ctx.fillRect(this.i, this.j, Math.abs(app.game.w) / 16, Math.abs(app.game.h) / 16)
}
}
// 绿色染料块
class Green extends Color{
constructor(i, j, color) {
super(i, j, color);
}
}
// 蓝色染料块
class Blue extends Color{
constructor(i, j, color) {
super(i, j, color);
}
}
// 红色染料块
class Red extends Color{
constructor(i, j, color) {
super(i, j, color);
}
}
// 灰色染料块,同时里面的正方形或者圆形
class Grey extends Color{
constructor(i, j, color, prop) {
super(i, j, color, prop);
}
}
// 地图中心点
class Main extends Item{
constructor(i, j, grids) {
super(i, j);
this.grids = grids
this.img = 'main';
this.w = 26;
this.init();
}
init(){
this.type = app.levels[app.level].type;
this.num = app.levels[app.level].num;
this.getNum = 0;
}
update(){
if(app.line){
app.map.drawImage(this.x + 39, this.y + 39, urlImg['main'], 26);
this.info(this.x, this.y)
}else{
new Array(4).fill(0).map((item, j) => {
new Array(4).fill(0).map((item, i) => {
this.grids[6 + j][6 + i].tool = 'main';
app.map.ctx.fillStyle = 'red';
app.map.ctx.fillRect(this.x + ((i + 6) * 6.5), this.y + ((j + 6) * 6.5), Math.abs(app.game.w) / 16, Math.abs(app.game.h) / 16)
})
})
app.map.drawImage((this.x + 26), this.y, urlImg['weizi'], 52);
}
}
info(x, y){
app.map.ctx.font = 'bold 1.5px Arial';
app.map.ctx.textAlign = 'left';
app.map.ctx.fillStyle = '#ffffff';
app.map.ctx.fillText('关卡', x + (6 * 6.5) + 3.5, y + (7 * 6.5) - 2.5);
app.map.ctx.fillText(app.level + 1, x + (6 * 6.5) + 4.5, y + (7 * 6.5) - 0.5);
app.map.drawImage((x + (6 * 6.5) + 4.5), y + (7 * 6.5) + 1, urlImg[this.type], 7.5);
// app.map.drawImage(
// x + (6 * 6.5) + 4.5,
// y + (7 * 6.5) + 1,
// urlImg[this.type],
// 7.5,
// 7.5,
// -30,
// 0,
// 60,
// 60
// )
app.map.ctx.font = 'bold 3px Arial';
app.map.ctx.textAlign = 'left';
app.map.ctx.fillStyle = '#555';
app.map.ctx.fillText('关卡', x + (8 * 6.5) - 1.8, y + (7 * 6.5));
app.map.ctx.fillText('解锁', x + (8 * 6.5) - 1.8, y + (9 * 6.5) - 2);
app.map.ctx.fillStyle = '#FD0752';
app.map.ctx.fillText(app.levels[app.level].info, x + (8 * 6.5) - 1.8, y + (9 * 6.5) + 1.5);
app.map.ctx.font = 'bold 5px Arial';
app.map.ctx.textAlign = 'left';
app.map.ctx.fillStyle = '#555';
app.map.ctx.fillText(this.getNum, x + (8 * 6.5) + 1.5, y + (8 * 6.5) - 2);
app.map.ctx.font = 'bold 3px Arial';
app.map.ctx.fillStyle = '#b1b1b1';
app.map.ctx.fillText("/" + this.num, x + (8 * 6.5) + 1.5, y + (8 * 6.5) + 1);
}
}
Grid类里新增一个 add方法 放入的生成的噪点里
// 添加噪点
add(){
if(this.show > 0 && !app.line) return;
if(this.first) return;
this.grids = this.grids.map((item, j) => {
return item.map((item, i) => {
if(item.if) {
let color = new cont[Object.keys(this.color)[0]](this.x + (i * 6.5), this.y + (j * 6.5), this.color, this.prop)
color.update();
item.color = color;
return item;
}
return item
})
})
this.first = true;
}
// canvas网格地图绘制里的 Grid 类里的 update
update(){
if(this.show > 0 && !app.line) return;
// 调用把不同类型的开采区添加进噪点
this.add();
app.map.begin();
this.grids.map((item, j) => {
item.map((item, i) => {
if(app.line){
app.map.ctx.strokeStyle = '#E3E7EA';
app.map.ctx.lineWidth = '0.2'
app.map.ctx.strokeRect(this.x + (i * 6.5), this.y + (j * 6.5), Math.abs(app.game.w) / 16, Math.abs(app.game.h) / 16);
}
if(item.if) {
item.color.update();
}
})
})
app.map.close();
}
let cont = {red: Red, green: Green, blue: Blue, grey: Grey};
别眨眼!看看完成出来的效果(图一原版,图二完成的效果),是不是有一种以假乱真甚至更好看的感觉!
地图工具放置
因为只写了第一关的原因,目前我只搞的工具也就是第一关和第二关的首先创建一个tools的大数组,里面保存的每个对象都代表一个工具,我来讲讲对象里面的每个属性都代表什么:
属性 | 代表的东西 |
---|---|
type | 工具类型 |
img | 工具的图片 |
level | 解锁这个工具的关卡 |
// 所有工具
let tools: [
{type: 'path', img: 'https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/89a739cc9118468cbb9d2a70a22bffc2~tplv-k3u1fbpfcp-watermark.image?', level: 0},
{type: '', img: '', level: 9},
{type: '', img: '', level: 9},
{type: 'miner', img: 'https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/df7bc18ef96c40af8e8ff74fbed9c5bb~tplv-k3u1fbpfcp-watermark.image?', level: 0},
{type: 'cut', img: 'https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/f6336eefefa94937adfe7e0ade7d25b1~tplv-k3u1fbpfcp-watermark.image?', level: 1},
{type: '', img: '', level: 9},
{type: '', img: '', level: 9},
{type: '', img: '', level: 9},
{type: '', img: '', level: 9},
{type: 'remove', img: 'https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/9934ebad4852405dab42d5e956c4d70f~tplv-k3u1fbpfcp-zoom-mark-crop-v2:460:460:0:0.awebp?', level: 1},
];
// 正在使用的工具保存
let tool = [];
// 方向数组
let dirs = [
'top',
'right',
'bottom',
'left',
];
// 现在的方向
let dir = 0;
然后就是和开采区的创建一样,一个主类,然后分出每个工具的继承类
// 工具通用继承Class
class Tool{
constructor(i, j) {
this.i = i;
this.j = j;
this.w = app.game.w / 16;
this.h = app.game.h / 16;
this.x = this.i * this.w;
this.y = this.j * this.h;
this.img = '';
this.pre = false;
this.timer = false;
this.goods = []
}
update(fn){
if(this.img === '') return;
app.map.begin();
fn && fn();
app.map.drawImage(this.x, this.y, urlImg[this.img], this.w);
app.map.close();
}
getNearby() {
return [
[this.i - 1, this.j, 'left'],
[this.i + 1, this.j, 'right'],
[this.i, this.j - 1, 'top'],
[this.i, this.j + 1, 'bottom'],
].map(item => {
return {
grid: app.game.getGrid(...item),
dir: item[2]
}
}).filter(item => item.grid.tool)
}
getNearbyGrid(){
return [
[this.i - 1, this.j, 'left'],
[this.i + 1, this.j, 'right'],
[this.i, this.j - 1, 'top'],
[this.i, this.j + 1, 'bottom'],
].map(item => {
return {
grid: app.game.getGrid(...item),
dir: item[2]
}
}).filter(item => item.grid)
}
}
// 开采器工具
class Miner extends Tool{
constructor(i, j, dir) {
super(i, j, dir);
this.img = 'miner';
this.dir = dir;
this.pre = false;
}
update(){
super.update(() => {
if(this.dir === 'right'){
this.rotate = 90;
}else if(this.dir === 'bottom'){
this.rotate = 180;
}else if(this.dir === 'left'){
this.rotate = 270
}else if(this.dir === 'top'){
this.rotate = 0;
}
app.map.ctx.globalAlpha = this.pre ? .4 : 1;
app.map.rotate(this.x, this.y, this.rotate);
})
}
}
// 剪切工具
class Cut extends Tool{
constructor(i, j, dir) {
super(i, j, dir);
this.img = 'cut'
this.dir = dir;
this.pre = false;
}
update(){
if(this.dir === 'right'){
this.rotate = 90;
}else if(this.dir === 'bottom'){
this.rotate = 180;
}else if(this.dir === 'left'){
this.rotate = 270
}else if(this.dir === 'top'){
this.rotate = 0;
}
app.map.begin();
app.map.ctx.globalAlpha = this.pre ? .4 : 1;
app.map.rotate(this.x, this.y, this.rotate);
console.log(this.w * this.wd, this.h * this.hd)
app.map.drawImage(this.x, this.y, urlImg[this.img], this.w * 2, this.h);
app.map.close();
}
}
// 画路工具
class Path extends Tool{
constructor(i, j, dir, nextDir) {
super(i, j, dir);
this.dir = dir;
this.img = '';
if (nextDir) {
this.nextDir = nextDir;
this.img = `path_${this.dir}_${this.nextDir}`;
return;
}
this.nextDir = app.reversePos(this.dir)
this.img = `path_${this.dir}_${this.nextDir}`;
}
update(){
super.update(() => {
app.map.ctx.globalAlpha = this.pre ? .4 : 1;
})
}
}
地图开采与运行
逻辑很浅显,就是开采器的放置位置是可开采的区域里,并且开采器的朝向旁边的路起始方向是一致的就能进行开采,那么这层逻辑有了,需要思考的就是怎么生成开采出的东西,还有中心收集点的正确收集判断了
判断是否达到生成的条件
class Tool{
// 生成开采物判断
isCorrect(){
if(this instanceof Miner) {
if(app.game.getGrid(this.i, this.j).color){
let nearby = this.getNearbyGrid().filter(item => {
if(item.grid.tool && (app.reversePos(item.grid.tool.dir) === this.dir)){
return true;
}else{
return false;
}
})
if(nearby.length < 1) return false;
return true
}
}
}
}
生成开采物
// 每个方块开采出来的东西
class Goods{
constructor(i ,j, type, dir) {
this.i = i;
this.j = j;
this.x = this.i * 6.5;
this.y = this.j * 6.5;
this.type = type;
this.dir = dir;
}
// 判断下一步该往哪走
update(){
this.i = ((parseFloat(this.x.toFixed(1)) + 6.5) / 6.5);
this.j = ((parseFloat(this.y).toFixed(1)) / 6.5);
let grid = app.game.getGrid(this.i, this.j);
if(((parseFloat(this.x.toFixed(1)) + 6.5) % 6.5 === 0) && ((parseFloat(this.y).toFixed(1)) % 6.5 === 0)){
if(grid.tool) this.dir = grid.tool.nextDir || grid.tool.dir;
else this.dir = '';
let nearby = [];
if(grid.tool !== 'main') nearby = grid.tool.getNearby().filter(item => {
return item.dir === this.dir && item.grid.tool
})
this.getMain(grid)
if(nearby.length < 1) this.dir = '';
}
app.map.drawImage(this.x + (8), this.y + (1.5), urlImg[this.type], 104 / 32);
// app.map.drawImage(this.x + (8), this.y + (1.5), urlImg[this.type], 104 / 32, 104 / 32, this.x + (8), this.y + (1.5), 104 / 32, 104 / 32);
}
// 是否传输进入中心点,并判断是否符合中心点需求
getMain(grid){
if(!grid.tool || grid.tool === 'main') return;
let nearby = grid.tool.getNearby().filter(item => {
return grid.tool && (grid.tool.nextDir === item.dir) && item.grid.tool === 'main';
})
if(nearby.length >= 1) {
if(this.type === app.game.get(40, 39).main.type){
setTimeout(() => {
app.game.get(40, 39).main.getNum += 1;
app.game.get(40, 39).main.info();
if(app.game.get(40, 39).main.getNum === app.game.get(40, 39).main.num){
app.level += 1
app.game.get(40, 39).main.init();
$('.rank').show();
app.game.get(40, 39).main.info();
}
}, 1000)
}
}
}
}
最终效果.
在线代码链接
结束语
目前截止到我准备发布这篇文章的时候,这个游戏其实也才做到第二关。但接下来关卡的制作无疑是飞速的,因为剩下需要考虑的知识变成了每个关卡解锁的工具使用上的思路编写了,如果这个星期的时间充裕的话我会抓经更新完剩下的关卡~ 当然上面的代码解析也只是讲了一些比较模糊的概念,如果掘友们有需要我进行详细解析的功能板块可以在评论区留言我会抓经和下几关的内容同时更新出一篇相关文章的!!也希望掘友们能为我的文章和在线代码 动动手指 点个赞!爱你们~