事情是这样的

有一天,我和我的小伙伴,深佬没事聊天,忽然就聊起了游戏开发,我们都对仙侠题材颇为感兴趣,于是我们一拍即合,决定做一个。

那么用什么开发?2d我熟,但是需要大量的美术资源,我们一时间做不了,所以我们只能想一个办法,一个可以把游戏做出来,同时资源什么的还容易弄的,交互还好的,于是想来想去,还是我的世界这种更加的适合,于是开始搞,使用Babylonjs,我们用了很短的时间,就把游戏的基本部分搞好了。

于是我把整个过程沉淀出来,分享给大家,,贡献出我们的力量,希望能有所帮助。

游戏在线预览地址,点这里,使用vue3+vite写的。

游戏源码仓库地址,点这里,放到了github上。

速通babylon基础功能

那么我们快速过一遍,基础的功能点。

创建场景

在游戏中,我们的内容都呈现在场景里,那么接下来,就介绍一下场景的创建过程

创建并获得canvas节点

html

<body><canvas id="renderCanvas"></canvas>
</body>
复制代码

JS

let canvas = document.getElementById("renderCanvas");
复制代码

创建一个engine引擎

有了canvas就能创建engine了

那么,引擎是什么?

对WebGL,audio等底层api进行封装,整合出更简便易用的接口。

代码实现

var createDefaultEngine = function () {return new BABYLON.Engine(canvas,true,{preserveDrawingBuffer: true,stencil: true,disableWebGL2Support: false});
};
复制代码

参数简单介绍一下

  • 第一个参数:把上面的canvas节点传入即可
  • 第二个参数:设置抗锯齿,默认是flase
  • 第三个参数:就是一些设置项,我直接用的官网例子中的配置方案,够用了。

可以创建场景了

有了engine,就能创建场景了

实例化一个场景

const scene = new BABYLON.Scene(window.engine);
复制代码

接下来为场景,加一个摄像机

那么,摄像机是啥?

我们看到的游戏画面,都是通过摄像机拍出来的,没有摄像机,就是漆黑一片。

代码实现:

this.vector = new Vector3(this.option.start.x,this.option.start.y,this.option.start.z
);
this.camera = new FreeCamera("Camera", this.vector, this.scene);
复制代码
  • Camera的类型有几种,这里我拿FreeCamera举例,因为我们做的游戏是第一人称,而这个FreeCamera非常适合。
  • 参数有三个
    • 第一个是相机实例的名称。
    • 第二个是三维坐标。
    • 第三个是要添加摄像机的场景,也就是我们刚刚创建的场景。

仅仅创建一个场景实例还不够,我们需要让engine引擎把scene渲染出来

window.engine.runRenderLoop(function () {if (scene && scene.activeCamera) {scene.render();}});
复制代码
  • runRenderLoop就是engine提供的渲染循环,传入一个渲染函数,那么引擎就会根据屏幕的刷新去回调这个函数(一秒会触发很多次这个函数,帧数越高,那么回调的次数就越多)。
  • 传入的函数逻辑很简单:判断场景是否存在和场景中是否添加了摄像机,成立的话,就调用场景实例的render这个api。

创建一个盒模型,天空和光

有了场景,就能在里面加入模型,天空和光了

创建盒模型

const box = BABYLON.MeshBuilder.CreateBox("box", {}, scene);
复制代码

效果图:

代码片段:https://code.juejin.cn/pen/7165452918270181384

蛤?怎么是漆黑的呢,能不能开下灯

创建光源

上面看到的黑色,并不是模型默认的颜色,而是没有光的缘故,那么我们开下灯。

代码实现:

const light = new BABYLON.HemisphericLight("light", new BABYLON.Vector3(0, 1, 0)
);
const light1 = new BABYLON.HemisphericLight("light1", new BABYLON.Vector3(0, -1, 0), scene);light1.intensity = 0.5;
复制代码
  • 为啥要创建两个光源,并且第二个光源设置了intensity

    • 因为光设置一个光源,会有一个面漆黑一片
    • 设置第二个光源目的是让这面不黑
    • 设置intensity目的是让底面看着暗一些,intensity表示强烈成都,0表示不发光,1表示最强光
  • 参数概述
    • 第一个参数是灯光实例的名称
    • 第二个参数就是坐标

效果:

马上掘金地址:https://code.juejin.cn/pen/7165406393954992128

创建蓝天

目前的环境背景是这样的,如图:

不好看,所以我们要做一个蓝天,来当背景。

先创建天空盒子

代码实现:

this.skyBox = MeshBuilder.CreateBox('skyBox', {width: width,height: height,depth: 1000}, this.scene
);
复制代码

给天空盒子加个材质,让他变成蓝天

那么,什么是材质?

材质,可以简单理解为,模型的面料,样子,贴图。

代码实现:

 let skyBox = BABYLON.MeshBuilder.CreateBox('SkyBox', {size:1000}, scene, false, BABYLON.Mesh.BACKSIDE);skyBox.material = new BABYLON.SkyMaterial('sky', scene);skyBox.material.inclination = -0.15;skyBox.material.backFaceCulling = false
复制代码
  • 使用BABYLON.MeshBuilder.CreateBox创建,

    • 第一个参数是模型的名字
    • 第二个是参数,其中仅仅设置可size为1000,代表着,width(宽),hight(高),depth(深)同时设置了1000
    • inclination表示太阳的日照,区间为[-0.5,0.5],
    • backFaceCulling这个叫剔除,你不设置false,看不到天空

效果:

代码片段:https://code.juejin.cn/pen/7165457880454266891

给盒模型添加材质

盒模型一共6个面,那么首先我们需要知道面的编号

效果如下:

代码片段:https://code.juejin.cn/pen/7165449658444021774

通过效果可知:

  • 0 前
  • 1 后
  • 2 左
  • 3 右
  • 4 上
  • 5 下

那么这个编号对应的就是faceUV的索引

faceUV就是配置六个面显示的数组

let faceUV = [new Vector4(1 / 4, 0, 2 / 4, 1), // 0号面new Vector4(1 / 4, 0, 2 / 4, 1), // 1号面new Vector4(2 / 4, 0, 3 / 4, 1), // 2号面new Vector4(2 / 4, 0, 3 / 4, 1), // 3号面new Vector4(0, 0, 1 / 4, 1), // 4号面new Vector4(3 / 4, 0, 1, 1), // 5号面
];
复制代码

我们设置一下材质所用的贴图

一张贴图如何切分给各个面 你需要了解Vector4,他的参数值是比例,参数都有:

  • x
  • y
  • z
  • w

em~~~,我其实也搞不懂,这个四维坐标是咋回事,但我将亲测出来的理解写下来,希望对你有用:

  • x和z,横向来看,你可以理解x表示截取图片的开始,z表示结束
  • y和w,纵向来看,你可以理解y是为截取图片的开始,w表示结束

那以 new Vector4(1 / 4, 0, 2 / 4, 1)这个为例,表示计算出截取图片的部分是:

  • 横向从1/4的位置到2 / 4

  • 纵向从0也就是图片的最下方。到1,也就是最高点

经过测试,和我的猜想是一致的。

效果:

代码片段:https://code.juejin.cn/pen/7165083410501730334

那么这个盒子就会作为接下来游戏开发中必不可少的角色,实现陆地的基本单元,命名为草地区块grassBlock

游戏设计

首先游戏设计讲起来是很复杂很繁琐的,由于篇幅有限,我用一个轻巧的方式,把游戏开发的整体思路给大家串一下,这样对阅读项目源码会很有帮助,源码我会在文章底部贴出。

架构设计

架构图

职能划分

面向对象的思路,单纯的前端开发来看,大部分都是UI开发,这些用面向过程的思路来做,往往就能应对大部分场景。但是在游戏开发中,不用面向对象我觉得是不可以的,那可太费劲了,所以整体都是面向对象的思路。

大体分三层:

  • Vue显示层:关注的页面呈现,装载babylon和卸载的工作。
  • Core核心层
    • gameEngine:实现事件循环,也就是每帧要做的工作,激活运转gameWidow和gameLogic
    • gameWidow:管理浏览器原生事件相关,对键盘事件,触摸事件等的管理。
    • gameLogic:主逻辑实现类,主要关注事件如何响应,创建游戏场景,设置重力,摄像机等游戏相关任务
  • World原料层
    • manager:管理器,针对同类别多种类的管理,统一管理创建,并对外提供接口获取所需的model对象(有点依赖注入的味道了,就像你去买苹果,你就不用一种苹果一种苹果的自己找,你直接找水果店老板,他就能告诉你什么好,什么适合,那么这个mananger就是干这个的)
    • models:模型层,作为组成单元被创建和组织,目前有模型盒子,材质单元,摄像机,大地图,天空,光源等。

项目结构

功能设计

创建地图,通过噪声算法

首先陆地的组成单位,就是上面我们创建好的草地区块grassBlock,我们需要通过一个个grassBlock拼出来我们的地图。

而噪声算法就是常见的用来生成地图的,直接拿来用,这里我简单说说为什么用噪声算法?

因为,我们需要一个办法,一个可以生成一个随机种子,往后通过这个种子就能生成固定的数据,并且具备连续性,数据是平滑的。

噪声算法算法就具备如下特点:

  • 随机性:创建的地图是随机的。
  • 可哈希:简单说作用,就是地图我们都是渲染我们视野范围内的,我们要保证离开视野之后,返回地图还是跟之前一致,不能再随机了。
  • 平滑,连续:随机出来的数据具有一点的连续性,不突兀,这样我们的地形就很平滑。

噪声算法代码如下:

class Random {private seed: number;// 实例化一个随机数生成器,seed=随机数种子,默认当前时间constructor(seed?: number) {this.seed = (seed || Date.now()) % 999999999;}// 返回0~1之间的数anext() {this.seed = (this.seed * 9301 + 49297) % 233280;return this.seed / 233280.0;}// 返回0~max之间的数nextInt(max: number) {return Math.floor(this.next() * max);}}export class PerlinNoiseGenerator {private static readonly F2: number = 0.3660254037844386;private static readonly G2: number = 0.21132486540518713;private static readonly F3: number = 1.0 / 3.0;private static readonly G3: number = 1.0 / 6.0;private static readonly GRAD3: Array<Array<number>> = [[1, 1, 0], [-1, 1, 0], [1, -1, 0], [-1, -1, 0],[1, 0, 1], [-1, 0, 1], [1, 0, -1], [-1, 0, -1],[0, 1, 1], [0, -1, 1], [0, 1, -1], [0, -1, -1],[1, 0, -1], [-1, 0, -1], [0, -1, 1], [0, 1, 1]];private readonly PERM: Array<number> = [];private readonly octaves: number;private readonly persistence: number;private readonly lacunarity: number;constructor(seed: number,option: { octaves: number, persistence: number, lacunarity: number }) {this.octaves = option.octaves;this.persistence = option.persistence;this.lacunarity = option.lacunarity;let random = new Random(seed);for (let i = 0; i < 512; i++) {this.PERM.push(random.nextInt(256));}}private dot3(v1: Array<number>, v2: Array<number>): number {return v1[0] * v2[0] + v1[1] * v2[1] + v1[2] * v2[2];}private noise2(x: number, y: number): number {let i1, j1, I, J;let s = (x + y) * PerlinNoiseGenerator.F2;let i = Math.floor(x + s);let j = Math.floor(y + s);let t = (i + j) * PerlinNoiseGenerator.G2;let xx = [];let yy = [];let f = [];let noise = [0.0, 0.0, 0.0];let g = [];xx[0] = x - (i - t);yy[0] = y - (j - t);i1 = xx[0] > yy[0] ? 1 : 0;j1 = xx[0] <= yy[0] ? 1 : 0;xx[2] = xx[0] + PerlinNoiseGenerator.G2 * 2.0 - 1.0;yy[2] = yy[0] + PerlinNoiseGenerator.G2 * 2.0 - 1.0;xx[1] = xx[0] - i1 + PerlinNoiseGenerator.G2;yy[1] = yy[0] - j1 + PerlinNoiseGenerator.G2;I = i & 255;J = j & 255;g[0] = this.PERM[I + this.PERM[J]] % 12;g[1] = this.PERM[I + i1 + this.PERM[J + j1]] % 12;g[2] = this.PERM[I + 1 + this.PERM[J + 1]] % 12;for (let c = 0; c <= 2; c++) {f[c] = 0.5 - xx[c] * xx[c] - yy[c] * yy[c];}for (let c = 0; c <= 2; c++) {if (f[c] > 0) {noise[c] = f[c] * f[c] * f[c] * f[c] *(PerlinNoiseGenerator.GRAD3[g[c]][0] * xx[c] + PerlinNoiseGenerator.GRAD3[g[c]][1] * yy[c]);}}return (noise[0] + noise[1] + noise[2]) * 70.0;}private noise3(x: number, y: number, z: number): number {let c, o1, o2, g = [], I, J, K;let f = [], noise = [0.0, 0.0, 0.0, 0.0];let s = (x + y + z) * PerlinNoiseGenerator.F3;let i = Math.floor(x + s);let j = Math.floor(y + s);let k = Math.floor(z + s);let t = (i + j + k) * PerlinNoiseGenerator.G3;let pos: Array<Array<number>> = [[], [], [], []];pos[0][0] = x - (i - t);pos[0][1] = y - (j - t);pos[0][2] = z - (k - t);if (pos[0][0] >= pos[0][1]) {if (pos[0][1] >= pos[0][2]) {o1 = [1, 0, 0];o2 = [1, 1, 0];} else if (pos[0][0] >= pos[0][2]) {o1 = [1, 0, 0];o2 = [1, 0, 1];} else {o1 = [0, 0, 1];o2 = [1, 0, 1];}} else {if (pos[0][1] < pos[0][2]) {o1 = [0, 0, 1];o2 = [0, 1, 1];} else if (pos[0][0] < pos[0][2]) {o1 = [0, 1, 0];o2 = [0, 1, 1];} else {o1 = [0, 1, 0];o2 = [1, 1, 0];}}for (c = 0; c <= 2; c++) {pos[3][c] = pos[0][c] - 1.0 + 3.0 * PerlinNoiseGenerator.G3;pos[2][c] = pos[0][c] - o2[c] + 2.0 * PerlinNoiseGenerator.G3;pos[1][c] = pos[0][c] - o1[c] + PerlinNoiseGenerator.G3;}I = i & 255;J = j & 255;K = k & 255;g[0] = this.PERM[I + this.PERM[J + this.PERM[K]]] % 12;g[1] = this.PERM[I + o1[0] + this.PERM[J + o1[1] + this.PERM[o1[2] + K]]] % 12;g[2] = this.PERM[I + o2[0] + this.PERM[J + o2[1] + this.PERM[o2[2] + K]]] % 12;g[3] = this.PERM[I + 1 + this.PERM[J + 1 + this.PERM[K + 1]]] % 12;for (c = 0; c <= 3; c++) {f[c] = 0.6 - pos[c][0] * pos[c][0] - pos[c][1] * pos[c][1] -pos[c][2] * pos[c][2];}for (c = 0; c <= 3; c++) {if (f[c] > 0) {noise[c] = f[c] * f[c] * f[c] * f[c] * this.dot3(pos[c], PerlinNoiseGenerator.GRAD3[g[c]]);}}return (noise[0] + noise[1] + noise[2] + noise[3]) * 32.0;}public simplex2(x: number, y: number): number {let freq = 1.0;let amp = 1.0;let max = 1.0;let total = this.noise2(x, y);for (let i = 1; i < this.octaves; i++) {freq *= this.lacunarity;amp *= this.persistence;max += amp;total += this.noise2(x * freq, y * freq) * amp;}return (1 + total / max) / 2;}public simplex3(x: number, y: number, z: number): number {let freq = 1.0;let amp = 1.0;let max = 1.0;let total = this.noise3(x, y, z);for (let i = 1; i < this.octaves; ++i) {freq *= this.lacunarity;amp *= this.persistence;max += amp;total += this.noise3(x * freq, y * freq, z * freq) * amp;}return (1 + total / max) / 2;}}复制代码

那么借助了噪声算法,我们就能够创建一个函数,传入x,z坐标,就能获得高的函数。

  private getHeight(x: number, y: number): number {let f = this.terrainPerlinNoise1.simplex2(x * 0.01, y * 0.01);let g = this.terrainPerlinNoise2.simplex2(-x * 0.01, -y * 0.01);let mh = g * 32;let h = f * mh;if (h <= 0) {h = 0;}return Math.floor(h);}复制代码

这样,我们只要遍历循环一个区域的x,z坐标传入这个函数获得y,就能获得一个区域内创建地图区块grassBlock的坐标数据了。

贴出

private initGroundBlocks(): void {let position = this.camera.getCurrentPosition();for (let i = 0; i < this.groundArrayLength; i++) {if (this.groundBlocksData[i] == null) {this.groundBlocksData[i] = [];}let a = 1;for (let j = 0; j < this.groundArrayLength; j++) {let x = position.x - this.visualField + i;let z = position.z - this.visualField + j;let y = this.getHeight(x, z);let isInCircle = isPointInCircle({ a: i, b: -j },{ a: this.visualField, b: -this.visualField },this.visualField);let blocks = [];if (isInCircle) {let grassBlock = this.blockLib.getGrassBlock().clone();grassBlock.setPosition(x, y, z);blocks.push(grassBlock);}this.groundBlocksData[i][j] = {x: x,y: y,z: z,show: isInCircle,blocks: blocks,};}}}
复制代码

只渲染视野范围内的

既然创建地图的功能已经好了,我们就来制定渲染地图的规则

  • 首先我们肯定不能全部渲染出来,我们需要渲染一部分,那么是哪一部分呢?

    • 全部渲染出来,卡爆了
  • 肯定是以摄像机这个第一视角为中心,以一个合适的视野距离画圆这个区域最为合理。

    • 这个视野距离就会作为半径画圆,目前设置的是36,很流程,扫出来的区域也够大
  • 也就是根据摄像机的中心坐标,开始计算一个圆形区域的坐标集合

    • 这块就是简单的遍历一个圆内的坐标集合,间隔单位就是grassBlock的宽度默认为1
  • 但是画圆有点难度,不如我们用一个取巧的方式,画一个方形,然后写个判定是否在圆内的工具函数来进行筛选,岂不是更容易。

    • 先画出一个方形,然后在用算法过滤,就这么简单

那么根据思路,我们一个圆形区域就画出来了

镜头可以碰撞,移动,跳跃

镜头就是第一人称,就是我们游戏内容人物。

首先镜头不可以穿透模型,能够遇到地形就挺住,这就需要具备碰撞能力

镜头需要移动,那么我们就需要实现键盘或者触摸事件的监听,来响应操作

当操作开始的时候,我们就通过引擎的事件循环,来进行对摄像机的移动,或者跳跃的处理

跳跃不能一直跳,跳到天上去,所以我们需要加入重力,这样我们就可以让他具备了向下运动的基本特征。

那么根据思路,就实现了一个在地形上奔腾的场景了。

结尾

这仅仅是一个开始,未来我们会不断的加入新的元素,让我们的世界变的更精彩。

游戏在线预览地址,点这里,使用vue3+vite写的。

游戏源码仓库地址,点这里,放到了github上。

【3d游戏开发】使用Babylonjs+Vue3搭建属于我们的小岛相关推荐

  1. DirectX 12 3D 游戏开发与实战第五章内容

    渲染流水线 学习目标: 了解用于在2D图像中表现出场景立体感和空间深度感等真实效果的关键因素 探索如何用Direct3D表示3D对象 学习如何建立虚拟摄像机 理解渲染流水线,根据给定的3D场景的几何描 ...

  2. 【Unity 3D游戏开发】在Unity使用NoSQL数据库方法介绍

    随着游戏体积和功能的不断叠加,游戏中的数据也变得越来越庞杂,这其中既包括玩家产生的游戏存档等数据,例如关卡数.金币等,也包括游戏配置数据,例如每一关的配置情况.尽管Unity提供了PlayerPref ...

  3. Unity3D ——强大的跨平台3D游戏开发工具教程

    http://unity3d.9ria.com/?p=22 众所周知,Unity3D是一个能够实现轻松创作的多平台的游戏开发工具,是一个全面整合的专业游戏引擎.在现有的版本中,其强大的游戏制作功能已经 ...

  4. 《Android 3D游戏开发技术宝典——OpenGL ES 2.0》.(吴亚峰).[PDF]ckook

    图书作者: 吴亚峰 图书编号: 9787115277701 图书格式: PDF 出 版 社: 人民邮电出版社 出版年份: 2012 图书页数: 700-800 [内容简介] 随着智能手机移动嵌入式平台 ...

  5. Laya进行3d游戏开发必了解

    laya进行3d游戏开发,需要使用unity导出模型,然后在laya中加载出来,初学者可能这一步会碰到很多问题,这里进行全方位的步骤带你进入laya3D世界 1:首先进入laya官网下载unity插件 ...

  6. Unity 3D游戏开发学习教程

    用C#用Unity3D制作游戏 你会学到: 您将学习3D游戏开发基础知识,以使用Unity3D引擎推进事物. 到本课程结束时,他们将可以轻松制作任何类型的游戏,无论是3D还是2D MP4 |视频:h2 ...

  7. 《Unity 3D 游戏开发技术详解与典型案例》——1.3节第一个Unity 3D程序

    本节书摘来自异步社区<Unity 3D 游戏开发技术详解与典型案例>一书中的第1章,第1.3节第一个Unity 3D程序,作者 吴亚峰 , 于复兴,更多章节内容可以访问云栖社区" ...

  8. 学习3D游戏开发进阶之路

    笔者从事IT行业15年了,一直奋斗在一线编程,从普通程序员逐步成长到上市公司技术总监,目前在创业公司担任技术合伙人,主要负责公司整个项目团队的技术管理.在网上或者论坛上很多同学请教过我关于如何学习3D ...

  9. 3D游戏开发套件指南(入门篇)

    今天将介绍最新的3D游戏开发套件.不论是使用2D还是3D游戏开发套件,都可以在不编写任何代码的情况下,通过设置与拖放便能快捷的实现游戏创意. 指南简介 本指南将引导开发者设置一个空的场景,使用3D游戏 ...

最新文章

  1. 三层交换机与路由器的主要区别
  2. 第二章 数据类型和文件操作
  3. VTK序列图像的读取
  4. h5策划书_一个好的H5营销活动设计要如何进行策划
  5. iOS layer 动画
  6. start running 开始跑步减肥
  7. Java Socket TeXT_FULL_WRITING 等问题解决
  8. python中的map()函数详解
  9. Java Web项目 配置 ueditor心得
  10. Android ViewFlipper源码分析
  11. DOM操作与引用资源的前后关系
  12. mysql join 去重_对mysql left join 出现的重复结果去重
  13. C语言 队列的实现(链表实现)
  14. 这样部署防病毒网关才妙啊!2000字详解奉上
  15. 软件工程毕设(四)·调研报告
  16. 线性代数 or 量子力学 ?(七——薛定谔方程详解)
  17. 开源ESB服务总线记录
  18. 【Google Play】Google Play 签名维护 ( 签名机制 | Google Play 签名机制选择 | 签名更新 )
  19. python构建决策引擎_决策引擎与机器学习模型的集成 | 信数这么干(一)
  20. Netty快速学习1-基础知识回顾

热门文章

  1. 语言判断(中文、英文、韩文等)
  2. js实现公告自动滚动
  3. 组件之间如何进行传值
  4. 通过自定义打印纸张的大小,实现打印到哪里纸张就停止在哪里。
  5. 计算机病毒攻击预警,【图片】计算机病毒红色预警 (危险度:极高)【mizukanainai吧】_百度贴吧...
  6. 【暑期集训第一周:搜索】【DFSBFS】
  7. Vitis-Ai 3.0 板卡镜像制作、模型量化编译教程
  8. 新手炒外汇,如何防止炒外汇被坑?
  9. ssm毕设项目木棉堂水果电商平台1r83i(java+VUE+Mybatis+Maven+Mysql+sprnig)
  10. 对接亚马逊 SP-API(Amazon Selling Partner API) 第一章:注册帐号