用webgl构建和谐的3D迷宫游戏

2016/09/19 · JavaScript
· WebGL

最早的文章出处:
AlloyTeam   

背景:这段日子本身竟然迷路了,有感而发就悟出写一个得以令人迷失的小游戏,可以消(bao)遣(fu)时(she)间(hui)

未曾动用threejs,就连glMatrix也未尝用,纯原生webgl干,写起来照旧挺累的,然而代码结构照旧挺清晰的,注释也挺全的,点开全文起初迷宫之旅~

究竟要赚一点PV,所以初始未有贴地址,以往贴地址:

github:

在线试玩:

打闹操作:鼠标调整方向,w前进,s后退,牢记方向键没用啊!

迷宫本身的相比较简陋,没加光和影子啥的,挺赶的三个demo。不过那篇小说不是介绍webgl技巧为主的,首要是教授整个娱乐开荒的场所,let’s
go~

 

1、生成2D迷宫

迷宫游戏嘛,肯定迷宫是重视。我们能够从游戏中来看,我们的迷宫分为2D迷宫和3D迷宫,首先说2D迷宫,它是3D迷宫的前提

变迁迷宫有二种办法

a)深度优先

一言不合贴源码:

先看一下用深度优先法生成迷宫的图吧

图片 1

大家看下迷宫的特征,发掘有一条很明朗的主路,是或不是能了然算法名中“深度优先”的意义了。简介一下算法的法则:

图片 2

明亮了规律,咱们初阶来制作2D迷宫~

第一得规定墙和路的涉嫌,思虑到迷宫转化为3D之后墙立体一点,大家就不要用1px的线来效仿墙了,那样3D之后相当不够充沛~

此处我们设置墙的薄厚为路的大幅,都以10px,然后大家的底图应该是那样子的(注:通晓那幅图最为关键):

图片 3

暗青部分是路,也足以清楚为原理中所说的邻格,那是能够达到规定的标准的

墨花青部分是墙,那些墙恐怕会发现,也恐怕未有开掘

镉绿部分是墙,那些墙是不容许打通的!!

假诺头脑没转过来就看下图,转化驾驭

图片 4

红线正是游戏的使用者的门路啦,当中大家来看穿过了多个草地绿的矩形,那正是地点所说的肉桂色格,可能打通,也大概没开掘,暗绿那块便是没发现的状态;而卡其色部分正对应下面的铁锈色格,那是不可能打通的,假诺把墙看成多个面(海洋蓝中绿部分再压缩),青蓝就产生三个点,是横墙与竖墙的交点,游戏的使用者不会走交点上面走的~

好,上边就是套算法的经过啦,写的经过中本身把墙的部分给省略了,全体考虑成路

JavaScript

var maxX = 18; var maxY = 13;

1
2
var maxX = 18;
var maxY = 13;

以大幅度来解释,Canvas宽度390px,有18列的路(20列的墙近年来被作者不在乎),不掌握的能够相比图看一下

initNeighbor方法是取得邻格用的,注意最终有多个自便,将它的邻格打乱,那样我们在getNeighbor
中获得邻格就很便利了

JavaScript

Grid.prototype.getNeighbor = function() {     var x, y, neighbor;
    this.choosed = true; // 标志当前格     for(var i = 0; i <
this.neighbor.length; i++) {         x = this.neighbor[i].x;         y
= this.neighbor[i].y;         neighbor = maze.grids[y][x];
        if(!neighbor.choosed) { // 邻格是不是标志过
            neighbor.parent = this; // 选中的邻格父级为日前格
            return neighbor;         }     }     if(this.parent ===
firstGrid) {         return 0; // 停止     } else {         return 1; //
这里是邻格都被标识过,再次来到父级     } };

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Grid.prototype.getNeighbor = function() {
    var x, y, neighbor;
    this.choosed = true; // 标记当前格
    for(var i = 0; i < this.neighbor.length; i++) {
        x = this.neighbor[i].x;
        y = this.neighbor[i].y;
        neighbor = maze.grids[y][x];
        if(!neighbor.choosed) { // 邻格是否标记过
            neighbor.parent = this; // 选中的邻格父级为当前格
            return neighbor;
        }
    }
    if(this.parent === firstGrid) {
        return 0; // 结束
    } else {
        return 1; // 这里是邻格都被标记过,返回父级
    }
};

这里相比较基本,注释给的也正如全,结合前面包车型大巴原理图应该很好懂

再看下maze里面的findPath方法,在这里面调用的getNeighbor方法

JavaScript

Maze.prototype.findPath = function() {     var tmp;     var curr =
firstGrid; // 先显著源点     while(1) {         tmp =
curr.getNeighbor(); // 获得邻格         if(tmp === 0) {
            console.log(‘路线寻觅截至’);             break;         }
else if(tmp === 1) { // 邻格都被标志,回到父级             curr =
curr.parent;         } else { // 找到了一个没被标识的邻格,存起来
            curr.children[curr.children.length] = tmp;
            curr = tmp;         }     } };

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Maze.prototype.findPath = function() {
    var tmp;
    var curr = firstGrid; // 先确定起点
    while(1) {
        tmp = curr.getNeighbor(); // 获得邻格
        if(tmp === 0) {
            console.log(‘路径找寻结束’);
            break;
        } else if(tmp === 1) { // 邻格都被标记,回到父级
            curr = curr.parent;
        } else { // 找到了一个没被标记的邻格,存起来
            curr.children[curr.children.length] = tmp;
            curr = tmp;
        }
    }
};

可以见到parent和children属性是还是不是本能的就反应起树的定义了,那不就是深浅的思虑么~

核心的代码讲授了,其余的美术部分就不介绍了,在drawPath方法里面,原理就是先画多少个节点(多个格子),然后它的children格和它打通(前面图古金色色格子转为黄色),再去画children格……

注:开始给的试玩demo用的不是深度优先算法,下边那个是深度优先生成的迷宫游戏,能够感受一下,那样与初始的有叁个比较

b)广度优先(prim随机)

一言不合贴源码:

再看一下广度优先生成的迷宫图~能够和地点的相比一下

图片 5

后边说的吃水优先算法蛮好了然的,人类语言表明出来正是“一直走,能走多少距离走多少距离,开掘不通了,死路了,再回到思虑办法”。

只是,用深度优先算法在迷宫游戏中有很致命的二个缺欠,就是轻便,那条明显的主路让游戏发烧友不看2D地形图都能轻轻巧松的绕出来(路痴退散),那眼看不符合发轫所说的消(bao)遣(fu)时(she)间(hui)的主旨,那么正主来啊~

prim(普Rim)算法是价值观迷宫游戏的正儿八经算法,岔路多,复杂。作者以为有广度优先的思虑,全部自身也称广度优先算法,正好和上四个对应上。贴原理图~

图片 6

人类语言表明出来正是“随机的诀要将地图上的墙尽也许打通”,还记得这些底图么,照着这么些底图我解释一下

图片 7

选取1为起源,并标识。1的邻墙有2,3,放入数组中。

那儿数组[2,
3],随机选拔贰个,比方我们选到了2,2的对面格是4,此时4一向不被标志过,有感而发就想到写多个方可令人迷失的小游戏。打通2(将2由灰形成紫藤色),并将4标记,并把5,6纳入数组

此刻数组[有感而发就想到写多个方可令人迷失的小游戏。2, 3, 5, 6],继续轻巧……

结合一下源码,发掘此番写法和上次的一心两样了,在深度优先中大家一向没思量墙的存在,宗旨是路(中灰的格子),将她们产生树的构培育能够,有感而发就想到写多个方可令人迷失的小游戏。在前面绘制部分再会设想墙的职位

而在广度优先中,小编感到大旨是墙(淡红的格子),所以算法中自然要把墙的定义带上,在initNeighbor方法中路(黄褐格)的邻格已经是+2并不是后面包车型的士+1了,因为+1是墙(黄褐格)

再重视大的getNeighbor方法

JavaScript

Grid.prototype.getNeighbor = function() {     var x, y, neighbor, ret =
[];     this.choosed = true;     for(var i = 0; i <
this.neighbor.length; i++) {         x = this.neighbor[i].x;         y
= this.neighbor[i].y;         neighbor = maze.grids[y][x];
        neighbor.wallX = this.x + (x – this.x)/2; // 重要!
        neighbor.wallY = this.y + (y – this.y)/2; // 重要!
        if(!neighbor.choosed) {             ret.push(neighbor);
        }     }     return ret; };

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Grid.prototype.getNeighbor = function() {
    var x, y, neighbor, ret = [];
    this.choosed = true;
    for(var i = 0; i < this.neighbor.length; i++) {
        x = this.neighbor[i].x;
        y = this.neighbor[i].y;
        neighbor = maze.grids[y][x];
        neighbor.wallX = this.x + (x – this.x)/2; // 重要!
        neighbor.wallY = this.y + (y – this.y)/2; // 重要!
        if(!neighbor.choosed) {
            ret.push(neighbor);
        }
    }
    return ret;
};

看起来我们获得的是邻格,但实在大家要的是挂载在邻格上的wallX和wallY属性,所以大家得以把neighbor抽象的就当做是墙!!在上边findPath方法中正是这么用的

JavaScript

Maze.prototype.findPath = function() {     var tmp;     var curr =
firstGrid;     var index;     var walls = this.walls;     tmp =
curr.getNeighbor();     curr.isClear = true; // 标记
    walls.push.apply(walls, tmp);     while(walls.length) {
        index = (Math.random() *有感而发就想到写多个方可令人迷失的小游戏。 walls.length) >> 0; // 随机取
        wall = walls[index];         if(!wall.isClear) { //
假若不是通路             wall.isClear = true;
            this.path.push({                 x: wall.wallX, // 主要!
                y: wall.wallY // 首要!             });             tmp
= wall.getNeighbor();             walls.push.apply(walls, tmp); //
加入更加的多的墙         } else {             walls.splice(index, 1); //
假如是通路了就移除         }     }     console.log(‘路径搜索甘休’,
this.path); };

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
Maze.prototype.findPath = function() {
    var tmp;
    var curr = firstGrid;
    var index;
    var walls = this.walls;
    tmp = curr.getNeighbor();
    curr.isClear = true; // 标记
    walls.push.apply(walls, tmp);
    while(walls.length) {
        index = (Math.random() * walls.length) >> 0; // 随机取
        wall = walls[index];
        if(!wall.isClear) { // 如果不是通路
            wall.isClear = true;
            this.path.push({
                x: wall.wallX, // 重要!
                y: wall.wallY // 重要!
            });
            tmp = wall.getNeighbor();
            walls.push.apply(walls, tmp); // 加入更多的墙
        } else {
            walls.splice(index, 1); // 如果是通路了就移除
        }
    }
    console.log(‘路径找寻结束’, this.path);
};

假使感觉有一点绕的话能够构成原理图再渐渐的看代码,核心绪解的一些正是getNeighbor方法再次回到的x,y对应是路(淡紫格)有感而发就想到写多个方可令人迷失的小游戏。,而它的wallX,wallY对应的是墙(冰雪蓝格)

画图部分很简短

JavaScript

for(i = 0; i <= 290; i+=20) { // 隔行画横线(横墙)     ctx.fillRect(0,
i, 390, 10); }   for(i = 0; i <= 390; i+=20) { // 隔行画竖线(竖墙)
    ctx.fillRect(i, 0, 10, 290); }   ctx.fillStyle = ‘white’;   for(i =
0; i < this.path.length; i++) { // 打通墙     ctx.fillRect(10 +
this.path[i].x * 10, 10 + this.path[i].y * 10, 10, 10); }

1
2
3
4
5
6
7
8
9
10
11
12
13
for(i = 0; i <= 290; i+=20) { // 隔行画横线(横墙)
    ctx.fillRect(0, i, 390, 10);
}
 
for(i = 0; i <= 390; i+=20) { // 隔行画竖线(竖墙)
    ctx.fillRect(i, 0, 10, 290);
}
 
ctx.fillStyle = ‘white’;
 
for(i = 0; i < this.path.length; i++) { // 打通墙
    ctx.fillRect(10 + this.path[i].x * 10, 10 + this.path[i].y * 10, 10, 10);
}

c)递归分割法

本条实际上是超级轻巧,原理轻便,算法简单,笔者就不介绍啦。一来这一个转换的迷宫也一流简单,一般不用于传统迷宫游戏;二来背后还会有为数非常的多要介绍的,不浪费口水在那了

 

2、生成3D迷宫

此刻大家已经有叁个2D迷宫,大家能够将其当作是俯视图,上面就是将其转会为3D巅峰新闻

注:那篇小说不承担介绍webgl!!笔者也尽也许回避webgl知识,通俗一点的介绍给大家~

将2D转3D,首先相当的重大的一些正是坐标系的转折

2D的坐标系是这么的

图片 8

3D的坐标系是那般的

图片 9

感到到到蛋疼就对了~前面思考到水墨画机近平面包车型客车冲击总结还得蛋碎呢~

骨子里这一个坐标转变并简单,首先大家先经过2D迷宫获得墙面的音讯(土灰部分)

下边这段代码是获得横墙音讯的

JavaScript

function getRowWall() {     var i = 0;     var j = 0;     var x1, x2;
    console.log(‘getRowWall’);     for(; i < height; i += 10) {
        rowWall[i] = [];         j = 0;         while(j < width)
{             if(isBlack(j, i)) {                 x1 = j; //
记录横墙起始点                                  j += 10;
                while(isBlack(j, i) && j < width) {
                    j += 10;                 }                   x2 = j;
// 记录横墙停止点                 if((x2 – x1) > 10) { //
那步很主要!!                     rowWall[i].push({
                        x1: 2 * (x1 / width) – 1,
                        x2: 2 * (x2 / width) – 1
                    });                 }             }               j
+= 10;         }     }       // console.log(rowWall); }

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
function getRowWall() {
    var i = 0;
    var j = 0;
    var x1, x2;
    console.log(‘getRowWall’);
    for(; i < height; i += 10) {
        rowWall[i] = [];
        j = 0;
        while(j < width) {
            if(isBlack(j, i)) {
                x1 = j; // 记录横墙开始点
                
                j += 10;
                while(isBlack(j, i) && j < width) {
                    j += 10;
                }
 
                x2 = j; // 记录横墙结束点
                if((x2 – x1) > 10) { // 这步很关键!!
                    rowWall[i].push({
                        x1: 2 * (x1 / width) – 1,
                        x2: 2 * (x2 / width) – 1
                    });
                }
            }
 
            j += 10;
        }
    }
 
    // console.log(rowWall);
}

结果会拿走三个数组,注意一下讲明中很主要的一步,为啥要大于10

上面两张图给您答案

图片 10

图片 11

小结正是小于等于10px的横墙,那它的本体一定是竖墙,10px也是那一行正美观到的,大家就将她们过滤掉了

收获竖墙音信同理,源码可知,小编就不贴出来了

上面这段代码是2D坐标转化为终点新闻

JavaScript

// k1和k2算作Z轴 for(i = 0; i < rowWall.length; i += 10) { //
rowWall.length     item = rowWall[i];     while((tmp = item.pop())) {
        k1 = (2 * i / height) – 1;         k2 = (2 * (i + 10) /
height) – 1;         po_data.push.apply(po_data, [
            tmp.x1*120+0.01, -1.09, k1*120, // 左下
            tmp.x2*120+0.01, -1.09, k1*120, // 右下
            tmp.x2*120+0.01, 0.2, k1*120, // 右上
            tmp.x1*120+0.01, 0.2, k1*120, // 左上  
            tmp.x2*120+0.01, -1.09, k1*120,
            tmp.x2*120+0.01, -1.09, k2*120,
            tmp.x2*120+0.01, 0.2, k2*120,
            tmp.x2*120+0.01, 0.2, k1*120,  
            tmp.x1*120+0.01, -1.09, k2*120,
            tmp.x2*120+0.01, -1.09, k2*120,
            tmp.x2*120+0.01, 0.2, k2*120,
            tmp.x1*120+0.01, 0.2, k2*120,  
            tmp.x1*120+0.01, -1.09, k1*120,
            tmp.x1*120+0.01, -1.09, k2*120,
            tmp.x1*120+0.01, 0.2, k2*120,
            tmp.x1*120+0.01, 0.2, k1*120,  
            tmp.x1*120+0.01, 0.2, k1*120,
            tmp.x2*120+0.01, 0.2, k1*120,
            tmp.x2*120+0.01, 0.2, k2*120,
            tmp.x1*120+0.01, 0.2, k1*120         ]);     } }

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// k1和k2算作Z轴
for(i = 0; i < rowWall.length; i += 10) { // rowWall.length
    item = rowWall[i];
    while((tmp = item.pop())) {
        k1 = (2 * i / height) – 1;
        k2 = (2 * (i + 10) / height) – 1;
        po_data.push.apply(po_data, [
            tmp.x1*120+0.01, -1.09, k1*120, // 左下
            tmp.x2*120+0.01, -1.09, k1*120, // 右下
            tmp.x2*120+0.01, 0.2, k1*120, // 右上
            tmp.x1*120+0.01, 0.2, k1*120, // 左上
 
            tmp.x2*120+0.01, -1.09, k1*120,
            tmp.x2*120+0.01, -1.09, k2*120,
            tmp.x2*120+0.01, 0.2, k2*120,
            tmp.x2*120+0.01, 0.2, k1*120,
 
            tmp.x1*120+0.01, -1.09, k2*120,
            tmp.x2*120+0.01, -1.09, k2*120,
            tmp.x2*120+0.01, 0.2, k2*120,
            tmp.x1*120+0.01, 0.2, k2*120,
 
            tmp.x1*120+0.01, -1.09, k1*120,
            tmp.x1*120+0.01, -1.09, k2*120,
            tmp.x1*120+0.01, 0.2, k2*120,
            tmp.x1*120+0.01, 0.2, k1*120,
 
            tmp.x1*120+0.01, 0.2, k1*120,
            tmp.x2*120+0.01, 0.2, k1*120,
            tmp.x2*120+0.01, 0.2, k2*120,
            tmp.x1*120+0.01, 0.2, k1*120
        ]);
    }
}

乘以120是自己3D空间中X轴和Z轴各拓展了120倍,未有写在模型转换矩阵里面,Y轴的不二诀要在模型变化矩阵中,可是那不首要。

数组中三个单位为某些,多个点为三个面,八个面为3D迷宫中一堵墙(底面包车型地铁无论是)

前面是webgl里面符合规律操作,各个矩阵、绑定buffer、绑定texture等等balabala,原生webgl写起来是比较累,无视了光和阴影还要写这么多T_T

 

3、录制机碰撞检查测量检验

假诺说前边的代码写着很累望着累,那这里的就更累了……

雕塑机是怎么样?在3D中录像机正是游戏的使用者的见识,正是通过鼠标和w,s来移动的webgl可视区,那么在2D中录制机映射为啥吗?

2D中摄像机正是新民主主义革命的十二分圈圈的右点,如图!

图片 12

这正是说大的局面只是便于看而已……

碰撞检查实验的遵守是幸免出现透视现象,透视现象如下图所示:

图片 13

要介绍透视现象出现的原故,就得先领会一下视锥体,如图:

图片 14

总的来看近平面了吗,当物体穿过近平面,就能够并发透视现象了

图片 15

大家娱乐中近平面距离是0.1,所以可能看成围绕原点有二个矩形,只要让矩形遇到不边,那就不会冒出透视现象

矩形的大幅我设置为2,设大了部分,也没要求让游戏用户贴墙贴的那么近……

小编们透过调用录制机的move方法触发Role.prototype.update方法

JavaScript

move: function(e){     // 只记挂x和z轴移动,cx,cy是改换为2D的动向
    cx = Math.sin(-this.rot) * e;     cy = Math.cos(-this.rot) * e;  
      this.x += cx;     this.z += cy;       ret =
role.check(-this.x/120, this.z/242, -cx, cy); // 后五个参数代表方向  
    if(ret.x === 0) {         this.x -= cx;     } else {         role.x
= ret.x;     }       if(ret.y === 0) {         this.z -= cy;     } else
{         role.y = ret.y;     }       role.update(); }

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
move: function(e){
    // 只考虑x和z轴移动,cx,cy是转换为2D的方向
    cx = Math.sin(-this.rot) * e;
    cy = Math.cos(-this.rot) * e;
 
 
    this.x += cx;
    this.z += cy;
 
    ret = role.check(-this.x/120, this.z/242, -cx, cy); // 后两个参数代表方向
 
    if(ret.x === 0) {
        this.x -= cx;
    } else {
        role.x = ret.x;
    }
 
    if(ret.y === 0) {
        this.z -= cy;
    } else {
        role.y = ret.y;
    }
 
    role.update();
}

而update方法里面更新x0,x2,y0,y2就是对应那八个点,那多少个点在check方法里面用到,check通过则运动录制机,不然不挪窝

水墨画机与墙的完好检查测验在Role.prototype.is沃尔中,注意这里有多少个参数,cx和cy,这一个是样子,确切的乃是就要移动的趋势,然后我们依照方向,只会从那多个点中取两个来决断会不会有碰撞

图片 16

种种点的质量评定通过Role.prototype.pointCheck方法,通过像平昔判定的,发现是油红值(rgb中的r为0)那么就以为撞上了,会在2D中标志浅绛红。倘使你贴着墙走,就能发觉葱绿的墙都被染成菘蓝啦~

 

结语:

写累死,这还是在把webgl里面知识点超越50%放弃的气象下。迷宫全部比较轻便,就两张贴图,地面也很简陋,这段时间急需非常多,很忙,没太多日子去美化。有意思味的同学能够做一款属于本身棒棒的迷宫游戏~

感兴趣有毛病的能够留言一齐调换~

4 赞 2 收藏
评论

图片 17

相关文章