译文:http://blog.jobbole.com/70956/

原文:http://www.playfuljs.com/a-first-person-engine-in-265-lines/

这是一篇关于利用 Canvas 实现3D 游戏场景绘制的文章,看完感觉很受启发,所以自己准备总结一下。我们先看下最终效果:

很酷是不是。原作者只使用了265行代码就实现了这么炫酷的效果。代码清晰明,目录结构简答(1个html, 4个图片),原文也对原理也进行了解释。本篇就直接将原作者的代码全部贴上来大家观赏观赏。


  1 <!doctype html>
  2 <html>
  3   <head>
  4     <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
  5     <title>Raycaster Demo - PlayfulJS</title>
  6   </head>
  7   <body style='background: #000; margin: 0; padding: 0; width: 100%; height: 100%;'>
  8     <canvas id='display' width='1' height='1' style='width: 100%; height: 100%;' />
  9
 10     <script>
 11
 12       var CIRCLE = Math.PI * 2;
 13       var MOBILE = /Android|webOS|iPhone|iPad|iPod|BlackBerry/i.test(navigator.userAgent)
 14
 15       function Controls() {
 16         this.codes  = { 37: 'left', 39: 'right', 38: 'forward', 40: 'backward' };
 17         this.states = { 'left': false, 'right': false, 'forward': false, 'backward': false };
 18         document.addEventListener('keydown', this.onKey.bind(this, true), false);
 19         document.addEventListener('keyup', this.onKey.bind(this, false), false);
 20         document.addEventListener('touchstart', this.onTouch.bind(this), false);
 21         document.addEventListener('touchmove', this.onTouch.bind(this), false);
 22         document.addEventListener('touchend', this.onTouchEnd.bind(this), false);
 23       }
 24
 25       Controls.prototype.onTouch = function(e) {
 26         var t = e.touches[0];
 27         this.onTouchEnd(e);
 28         if (t.pageY < window.innerHeight * 0.5) this.onKey(true, { keyCode: 38 });
 29         else if (t.pageX < window.innerWidth * 0.5) this.onKey(true, { keyCode: 37 });
 30         else if (t.pageY > window.innerWidth * 0.5) this.onKey(true, { keyCode: 39 });
 31       };
 32
 33       Controls.prototype.onTouchEnd = function(e) {
 34         this.states = { 'left': false, 'right': false, 'forward': false, 'backward': false };
 35         e.preventDefault();
 36         e.stopPropagation();
 37       };
 38
 39       Controls.prototype.onKey = function(val, e) {
 40         var state = this.codes[e.keyCode];
 41         if (typeof state === 'undefined') return;
 42         this.states[state] = val;
 43         e.preventDefault && e.preventDefault();
 44         e.stopPropagation && e.stopPropagation();
 45         console.log(e.keyCode);
 46       };
 47
 48       function Bitmap(src, width, height) {
 49         this.image = new Image();
 50         this.image.src = src;
 51         this.width = width;
 52         this.height = height;
 53       }
 54
 55       function Player(x, y, direction) {
 56         this.x = x;
 57         this.y = y;
 58         this.direction = direction;
 59         this.weapon = new Bitmap('assets/knife_hand.png', 319, 320);
 60         this.paces = 0;
 61       }
 62
 63       Player.prototype.rotate = function(angle) {
 64         this.direction = (this.direction + angle + CIRCLE) % (CIRCLE);
 65       };
 66
 67       Player.prototype.walk = function(distance, map) {
 68         var dx = Math.cos(this.direction) * distance;
 69         var dy = Math.sin(this.direction) * distance;
 70         if (map.get(this.x + dx, this.y) <= 0) this.x += dx;
 71         if (map.get(this.x, this.y + dy) <= 0) this.y += dy;
 72         this.paces += distance;
 73       };
 74
 75       Player.prototype.update = function(controls, map, seconds) {
 76         if (controls.left) this.rotate(-Math.PI * seconds);
 77         if (controls.right) this.rotate(Math.PI * seconds);
 78         if (controls.forward) this.walk(3 * seconds, map);
 79         if (controls.backward) this.walk(-3 * seconds, map);
 80       };
 81
 82       function Map(size) {
 83         this.size = size;
 84         this.wallGrid = new Uint8Array(size * size);
 85         this.skybox = new Bitmap('assets/deathvalley_panorama.jpg', 2000, 750);
 86         this.wallTexture = new Bitmap('assets/wall_texture.jpg', 1024, 1024);
 87         this.light = 0;
 88       }
 89
 90       Map.prototype.get = function(x, y) {
 91         x = Math.floor(x);
 92         y = Math.floor(y);
 93         if (x < 0 || x > this.size - 1 || y < 0 || y > this.size - 1) return -1;
 94         return this.wallGrid[y * this.size + x];
 95       };
 96
 97       Map.prototype.randomize = function() {
 98         for (var i = 0; i < this.size * this.size; i++) {
 99           this.wallGrid[i] = Math.random() < 0.3 ? 1 : 0;
100         }
101       };
102
103       Map.prototype.cast = function(point, angle, range) {
104         var self = this;
105         var sin = Math.sin(angle);
106         var cos = Math.cos(angle);
107         var noWall = { length2: Infinity };
108
109         return ray({ x: point.x, y: point.y, height: 0, distance: 0 });
110
111         function ray(origin) {
112           var stepX = step(sin, cos, origin.x, origin.y);
113           var stepY = step(cos, sin, origin.y, origin.x, true);
114           var nextStep = stepX.length2 < stepY.length2
115             ? inspect(stepX, 1, 0, origin.distance, stepX.y)
116             : inspect(stepY, 0, 1, origin.distance, stepY.x);
117
118           if (nextStep.distance > range) return [origin];
119           return [origin].concat(ray(nextStep));
120         }
121
122         function step(rise, run, x, y, inverted) {
123           if (run === 0) return noWall;
124           var dx = run > 0 ? Math.floor(x + 1) - x : Math.ceil(x - 1) - x;
125           var dy = dx * (rise / run);
126           return {
127             x: inverted ? y + dy : x + dx,
128             y: inverted ? x + dx : y + dy,
129             length2: dx * dx + dy * dy
130           };
131         }
132
133         function inspect(step, shiftX, shiftY, distance, offset) {
134           var dx = cos < 0 ? shiftX : 0;
135           var dy = sin < 0 ? shiftY : 0;
136           step.height = self.get(step.x - dx, step.y - dy);
137           step.distance = distance + Math.sqrt(step.length2);
138           if (shiftX) step.shading = cos < 0 ? 2 : 0;
139           else step.shading = sin < 0 ? 2 : 1;
140           step.offset = offset - Math.floor(offset);
141           return step;
142         }
143       };
144
145       Map.prototype.update = function(seconds) {
146         if (this.light > 0) this.light = Math.max(this.light - 10 * seconds, 0);
147         else if (Math.random() * 5 < seconds) this.light = 2;
148       };
149
150       function Camera(canvas, resolution, focalLength) {
151         this.ctx = canvas.getContext('2d');
152         this.width = canvas.width = window.innerWidth * 0.5;
153         this.height = canvas.height = window.innerHeight * 0.5;
154         this.resolution = resolution;
155         this.spacing = this.width / resolution;
156         this.focalLength = focalLength || 0.8;
157         this.range = MOBILE ? 8 : 14;
158         this.lightRange = 5;
159         this.scale = (this.width + this.height) / 1200;
160       }
161
162       Camera.prototype.render = function(player, map) {
163         this.drawSky(player.direction, map.skybox, map.light);
164         this.drawColumns(player, map);
165         this.drawWeapon(player.weapon, player.paces);
166       };
167
168       Camera.prototype.drawSky = function(direction, sky, ambient) {
169         var width = sky.width * (this.height / sky.height) * 2;
170         var left = (direction / CIRCLE) * -width;
171
172         this.ctx.save();
173         this.ctx.drawImage(sky.image, left, 0, width, this.height);
174         if (left < width - this.width) {
175           this.ctx.drawImage(sky.image, left + width, 0, width, this.height);
176         }
177         if (ambient > 0) {
178           this.ctx.fillStyle = '#ffffff';
179           this.ctx.globalAlpha = ambient * 0.1;
180           this.ctx.fillRect(0, this.height * 0.5, this.width, this.height * 0.5);
181         }
182         this.ctx.restore();
183       };
184
185       Camera.prototype.drawColumns = function(player, map) {
186         this.ctx.save();
187         for (var column = 0; column < this.resolution; column++) {
188           var x = column / this.resolution - 0.5;
189           var angle = Math.atan2(x, this.focalLength);
190           var ray = map.cast(player, player.direction + angle, this.range);
191           this.drawColumn(column, ray, angle, map);
192         }
193         this.ctx.restore();
194       };
195
196       Camera.prototype.drawWeapon = function(weapon, paces) {
197         var bobX = Math.cos(paces * 2) * this.scale * 6;
198         var bobY = Math.sin(paces * 4) * this.scale * 6;
199         var left = this.width * 0.66 + bobX;
200         var top = this.height * 0.6 + bobY;
201         this.ctx.drawImage(weapon.image, left, top, weapon.width * this.scale, weapon.height * this.scale);
202       };
203
204       Camera.prototype.drawColumn = function(column, ray, angle, map) {
205         var ctx = this.ctx;
206         var texture = map.wallTexture;
207         var left = Math.floor(column * this.spacing);
208         var width = Math.ceil(this.spacing);
209         var hit = -1;
210
211         while (++hit < ray.length && ray[hit].height <= 0);
212
213         for (var s = ray.length - 1; s >= 0; s--) {
214           var step = ray[s];
215           var rainDrops = Math.pow(Math.random(), 3) * s;
216           var rain = (rainDrops > 0) && this.project(0.1, angle, step.distance);
217
218           if (s === hit) {
219             var textureX = Math.floor(texture.width * step.offset);
220             var wall = this.project(step.height, angle, step.distance);
221
222             ctx.globalAlpha = 1;
223             ctx.drawImage(texture.image, textureX, 0, 1, texture.height, left, wall.top, width, wall.height);
224
225             ctx.fillStyle = '#000000';
226             ctx.globalAlpha = Math.max((step.distance + step.shading) / this.lightRange - map.light, 0);
227             ctx.fillRect(left, wall.top, width, wall.height);
228           }
229
230           ctx.fillStyle = '#ffffff';
231           ctx.globalAlpha = 0.15;
232           while (--rainDrops > 0) ctx.fillRect(left, Math.random() * rain.top, 1, rain.height);
233         }
234       };
235
236       Camera.prototype.project = function(height, angle, distance) {
237         var z = distance * Math.cos(angle);
238         var wallHeight = this.height * height / z;
239         var bottom = this.height / 2 * (1 + 1 / z);
240         return {
241           top: bottom - wallHeight,
242           height: wallHeight
243         };
244       };
245
246       function GameLoop() {
247         this.frame = this.frame.bind(this);
248         this.lastTime = 0;
249         this.callback = function() {};
250       }
251
252       GameLoop.prototype.start = function(callback) {
253         this.callback = callback;
254         requestAnimationFrame(this.frame);
255       };
256
257       GameLoop.prototype.frame = function(time) {
258         var seconds = (time - this.lastTime) / 1000;
259         this.lastTime = time;
260         if (seconds < 0.2) this.callback(seconds);
261         requestAnimationFrame(this.frame);
262       };
263
264       var display = document.getElementById('display');
265       var player = new Player(15.3, -1.2, Math.PI * 0.3);
266       var map = new Map(32);
267       var controls = new Controls();
268       var camera = new Camera(display, MOBILE ? 160 : 320, 0.8);
269       var loop = new GameLoop();
270
271       map.randomize();
272
273       loop.start(function frame(seconds) {
274         map.update(seconds);
275         player.update(controls.states, map, seconds);
276         camera.render(player, map);
277       });
278
279     </script>
280     <script>
281       (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
282       (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
283       m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
284       })(window,document,'script','//www.google-analytics.com/analytics.js','ga');
285
286       ga('create', 'UA-50885475-1', 'playfuljs.com');
287       ga('send', 'pageview');
288     </script>
289   </body>
290 </html>

我把译文粘贴过来。下为译文。


本文由 伯乐在线 - Mxt 翻译。未经许可,禁止转载!
英文出处:playfuljs。欢迎加入翻译组。

今天,让我们进入一个可以伸手触摸的世界吧。在这篇文章里,我们将从零开始快速完成一次第一人称探索。本文没有涉及复杂的数学计算,只用到了光线投射技术。你可能已经见识过这种技术了,比如《上古卷轴2 : 匕首雨》、《毁灭公爵3D》还有 Notch Persson 最近在 ludum dare 上的参赛作品。Notch 认为它够好,我就认为它够好!

[Demo (arrow keys / touch)] [Source]

用了光线投射就像开挂一样,作为一名懒得出油的程序员,我表示非常喜欢。你可以舒畅地浸入到3D环境中而不受“真3D”复杂性的束缚。举例来说,光线投射算法消耗线性时间,所以不用优化也可以加载一个巨大的世界,它执行的速度跟小型世界一样快。水平面被定义成简单的网格而不是多边形网面树,所以即使没有 3D 建模基础或数学博士学位也可以直接投入进去学习。

利用这些技巧很容易就可以做一些让人嗨爆的事情。15分钟之后,你会到处拍下你办公室的墙壁,然后检查你的 HR 文档看有没有规则禁止“工作场所枪战建模”。

玩家

我们从何处投射光线?这就是玩家对象(Player)的作用,只需要三个属性 x,y,direction。

JavaScript
1
2
3
4
5

function Player(x, y, direction) {
  this.x = x;
  this.y = y;
  this.direction = direction;
}

地图

我们将地图存作简单的二维数组。数组中,0代表没墙,1代表有墙。你还可以做得更复杂些,比如给墙设任意高度,或者将多个墙数据的“楼层(stories)”打包进数组。但作为我们的第一次尝试,用0-1就足够了。

JavaScript
1
2
3
4

function Map(size) {
  this.size = size;
  this.wallGrid = new Uint8Array(size * size);
}

投射一束光线

这里就是窍门:光线投射引擎不会一次性绘制出整个场景。相反,它把场景分成独立的列然后一条一条地渲染。每一列都代表从玩家特定角度投射出的一条光线。如果光线碰到墙壁,引擎会计算玩家到墙的距离然后在该列中画出一个矩形。矩形的高度取决于光线的长度——越远则越短。

绘画的光线越多,显示效果就会越平滑。

1. 找到每条光线的角度

我们首先找出每条光线投射的角度。角度取决于三点:玩家面向的方向,摄像机的视野,还有正在绘画的列。

JavaScript
1
2

var angle = this.fov * (column / this.resolution - 0.5);
var ray = map.cast(player, player.direction + angle, this.range);

2. 通过网格跟踪每条光线

接下来,我们要检查每条光线经过的墙。这里的目标是最终得出一个数组,列出了光线离开玩家后经过的每面墙。

从玩家开始,我们找出最接近的横向(stepX)和纵向(stepY)网格坐标线。移到最近的地方然后检查是否有墙(inspect)。一直重复检查直到跟踪完每条线的所有长度。

JavaScript
1
2
3
4
5
6
7
8
9
10

function ray(origin) {
  var stepX = step(sin, cos, origin.x, origin.y);
  var stepY = step(cos, sin, origin.y, origin.x, true);
  var nextStep = stepX.length2 < stepY.length2
    ? inspect(stepX, 1, 0, origin.distance, stepX.y)
    : inspect(stepY, 0, 1, origin.distance, stepY.x);
  if (nextStep.distance > range) return [origin];
  return [origin].concat(ray(nextStep));
}

寻找网格交点很简单:只需要对 x 向下取整(1,2,3…),然后乘以光线的斜率(rise/run)得出 y。

JavaScript
1
2

var dx = run > 0 ? Math.floor(x + 1) - x : Math.ceil(x - 1) - x;
var dy = dx * (rise / run);

现在看出了这个算法的亮点没有?我们不用关心地图有多大!只需要关注网格上特定的点——与每帧的点数大致相同。样例中的地图是32×32,而32,000×32,000的地图一样跑得这么快!

3. 绘制一列

跟踪完一条光线后,我们就要画出它在路径上经过的所有墙。

JavaScript
1
2

var z = distance * Math.cos(angle);
var wallHeight = this.height * height / z;

我们通过墙高度的最大除以 z 来觉得它的高度。越远的墙,就画得越短。

额,这里用 cos 是怎么回事?如果直接使用原来的距离,就会产生一种超广角的效果(鱼眼镜头)。为什么?想象你正面向一面墙,墙的左右边缘离你的距离比墙中心要远。于是原本直的墙中心就会膨胀起来了!为了以我们真实所见的效果去渲染墙面,我们通过投射的每条光线一起构建了一个三角形,通过 cos 算出垂直距离。如图:

我向你保证,这里已经是本文最难的数学啦。

渲染出来

我们用摄像头对象 Camera 从玩家视角画出地图的每一帧。当我们从左往右扫过屏幕时它会负责渲染每一列。

在绘制墙壁之前,我们先渲染一个天空盒(skybox)——就是一张大的背景图,有星星和地平线,画完墙后我们还会在前景放个武器。
JavaScript
1
2
3
4
5

Camera.prototype.render = function(player, map) {
  this.drawSky(player.direction, map.skybox, map.light);
  this.drawColumns(player, map);
  this.drawWeapon(player.weapon, player.paces);
};

摄像机最重要的属性是分辨率(resolution)、视野(fov)和射程(range)。

  • 分辨率决定了每帧要画多少列,即要投射多少条光线。
  • 视野决定了我们能看的宽度,即光线的角度。
  • 射程决定了我们能看多远,即光线长度的最大值

组合起来

使用控制对象 Controls 监听方向键(和触摸事件)。使用游戏循环对象 GameLoop 调用 requestAnimationFrame 请求渲染帧。这里的 gameloop 只有三行

JavaScript
1
2
3
4
5

oop.start(function frame(seconds) {
  map.update(seconds);
  player.update(controls.states, map, seconds);
  camera.render(player, map);
});

细节

雨滴

雨滴是用大量随机放置的短墙模拟的。

JavaScript
1
2
3
4
5
6

var rainDrops = Math.pow(Math.random(), 3) * s;
var rain = (rainDrops > 0) && this.project(0.1, angle, step.distance);
ctx.fillStyle = '#ffffff';
ctx.globalAlpha = 0.15;
while (--rainDrops > 0) ctx.fillRect(left, Math.random() * rain.top, 1, rain.height);

这里没有画出墙完全的宽度,而是画了一个像素点的宽度。

照明和闪电

照明其实就是明暗处理。所有的墙都是以完全亮度画出来,然后覆盖一个带有一定不透明度的黑色矩形。不透明度决定于距离与墙的方向(N/S/E/W)。

JavaScript
1
2
3

ctx.fillStyle = '#000000';
ctx.globalAlpha = Math.max((step.distance + step.shading) / this.lightRange - map.light, 0);
ctx.fillRect(left, wall.top, width, wall.height);

要模拟闪电,map.light 随机达到2然后再快速地淡出。

碰撞检测

要防止玩家穿墙,我们只要用他要到的位置跟地图比较。分开检查 x 和 y 玩家就可以靠着墙滑行。

JavaScript
1
2
3
4
5
6

Player.prototype.walk = function(distance, map) {
  var dx = Math.cos(this.direction) * distance;
  var dy = Math.sin(this.direction) * distance;
  if (map.get(this.x + dx, this.y) <= 0) this.x += dx;
  if (map.get(this.x, this.y + dy) <= 0) this.y += dy;
};

墙壁贴图

没有贴图(texture)的墙面看起来会比较无趣。但我们怎么把贴图的某个部分对应到特定的列上?这其实很简单:取交叉点坐标的小数部分。

JavaScript
1
2

step.offset = offset - Math.floor(offset);
var textureX = Math.floor(texture.width * step.offset);

举例来说,一面墙上的交点为(10,8.2),于是取小数部分0.2。这意味着交点离墙左边缘20%远(8),离墙右边缘80%远(9)。所以我们用 0.2 * texture.width 得出贴图的 x 坐标。

试一试

在恐怖废墟中逛一逛。
还有人扩展了社区版。
  • ctolsen添加了 WASD 方向键。
Fredrik Wallgren 实现了 Java 移植。

接下来做什么?

因为光线投射器是如此地快速、简单,你可以快速地实现许多想法。你可以做个地牢探索者(Dungeon Crawler)、第一人称射手、或者侠盗飞车式沙盒。靠!常数级的时间消耗真让我想做一个老式的大型多人在线角色扮演游戏,包含大量的、程序自动生成的世界。这里有一些带你起步的难题:

  • 浸入式体验。样例在求你为它加上全屏、鼠标定位、下雨背景和闪电时同时出现雷响。
  • 室内级别。用对称渐变取代天空盒。或者,你觉得自己很屌的话,尝试用瓷片渲染地板和天花板。(可以这么想:所有墙面画出来之后,画面剩下的空隙就是地板和天花板了)
  • 照明对象。我们已经有了一个相当健壮的照明模型。为何不将光源放到地图上,通过它们计算墙的照明?光源占了80%大气层。
  • 良好的触摸事件。我已经搞定了一些基本的触摸操作,手机和平板的小伙伴们可以尝试一样 demo。但这里还有巨大的提升空间。
  • 摄像机特效。比如放大缩小、模糊、醉汉模式等等。有了光线投射器这些都显得特别简单。先从控制台中修改 camera.fov 开始。

同往常一样,如果你造了什么炫爆的东西或者有什么相关的研究要分享,发 email 给我或 tweet 我,我会分享给大家的。

讨论

Hacker News 上的讨论。

  • Raycasting in Comanche -高度地图光线投射超棒的例子。

感谢

本来打算写两个钟的文章结果写了三周。没有以下的帮助我不可能写完这篇文章:

  • Jim Snodgrass: editing & feedback
  • Jeremy Morrell: editing & feedback
  • Jeff Peterson: editing & feedback
  • Chris Gomez: weapons & feedback
  • Amanda Lenz: laptop bags and support
  • Nicholas S: wall texture
  • Dan Duriscoe: Death Valley skybox

转载于:https://www.cnblogs.com/muyun/p/5094992.html

【转】265行JavaScript代码的第一人称3D H5游戏Demo相关推荐

  1. 爆肝一周,完成了一款第一人称3D射击游戏,现在把源代码分享给大家,适合新手跟着学习

    一.项目背景 大家好,我是小飞 我之前给大家分享了一些网上现成的游戏模板资源,但是很多小伙伴后来反馈说网上的资源有些不能用了,导入进去就会报错等等的问题. 还有一些小伙伴私信问我,有没有什么3D类的游 ...

  2. [转]爆肝一周,完成了一款第一人称3D射击游戏,现在把源代码分享给大家,适合新手跟着学习

    一.项目背景 大家好,我是小飞 我之前给大家分享了一些网上现成的游戏模板资源,但是很多小伙伴后来反馈说网上的资源有些不能用了,导入进去就会报错等等的问题. 还有一些小伙伴私信问我,有没有什么3D类的游 ...

  3. 60行JavaScript代码写俄罗斯方块

    教你看懂网上流传的60行JavaScript代码俄罗斯方块游戏 早就听说网上有人仅仅用60行JavaScript代码写出了一个俄罗斯方块游戏,最近看了看,今天在这篇文章里面我把我做的分析整理一下(主要 ...

  4. 只要200行JavaScript代码,就能把特斯拉汽车带到您身边

    Jerry的前一篇文章 如何使用JavaScript开发AR(增强现实)移动应用 (一) 介绍了用React-Native + ViroReact开发增强现实应用的一些预备知识. 本文咱们开始进入增强 ...

  5. 100行JavaScript代码实现JavaScript

    先看效果: 100行JavaScript代码实现经典游戏俄罗斯方块 新建一个html文件,复制如下代码,用浏览器打开即可: <!doctype html> <html> < ...

  6. [原创]22行JavaScript代码实现QQ群成员提取器,绿色、环保、无病毒!

    原来想给QQ群内的成员发邮件,找了一个现成的软件,没想到居然有QQ盗号病毒,很不爽.就决定自己动手,丰衣足食. 首先,我想到如果能够直接从QQ群成员列表中把所有成员拷贝出来,岂不是很方便.不过QQ明显 ...

  7. python50行小游戏_50行python代码实现的贪吃蛇小游戏

    50行python代码实现的贪吃蛇小游戏 发布于 2014-09-01 21:26:24 | 1337 次阅读 | 评论: 1 | 来源: 网友投递 Python编程语言Python 是一种面向对象. ...

  8. fps射击HTML网页游戏,关于Unity中FPS第一人称射击类游戏制作(专题十)

    当前Unity最新版本5.6.3f1,我使用的是5.5.1f1 FPS第一人称射击类游戏实例 场景搭建 1.创建Unity项目工程和文件目录,保存场景 2.导入人物模型和子弹碎片的资源包charact ...

  9. HTML5第一人称3D艺术画廊js特效

    下载地址 HTML5第一人称3D艺术画廊特效是一款带左右箭头按钮,以第一人称视角参观的3D艺术画廊,操控鼠标可以控制方向,看到不同方向的画品展示. dd:

最新文章

  1. 官宣!这些北京高校,去雄安!
  2. python server酱_面向回家编程!GitHub标星两万的quot;Python抢票神器”快用起来!...
  3. [hihoCoder]无间道之并查集
  4. PHP7新增的主要特性
  5. 《浅谈架构之路:前后端分离模式》
  6. Redis持久化_Redis事务_Redis删除策略
  7. SpringBoot 集成 MyBatisPlus 模板
  8. 图论算法——图的遍历
  9. 出现ESXi系统无法连接FreeNAS的情况?90%以上的人都做错了!
  10. MySql可视化工具MySQL Workbench使用笔记
  11. 深信服桌面云盒子需要服务器吗,为何众多客户选择深信服桌面云?主要看实力...
  12. Zoom支持自动生成字幕;SharePlay上线;Safari 更新导致大量bug |WebRTC风向
  13. 11年的macbook还能用吗_8年老本的第二春:2011款Macbook Pro换血记
  14. Word2010邮件合并(附带照片)
  15. 《苏菲的世界》——读书笔记
  16. python用turtle画小猪佩奇_python 用turtle 画小猪佩奇
  17. 检测分割算法改进(篇一) YoloX和Yolov3-v5之FCOUS模块、CSP模块、SPP模块
  18. 1232: [Usaco2008Nov]安慰奶牛cheer
  19. 如何破译Charles
  20. Python对Excel文件进行多行求和并将结果排序输出前三

热门文章

  1. 华为USG防火墙-建立安全策略禁止上班时间访问其他网站
  2. 周鸿祎:未来两年无线互联网格局将定
  3. spring应用集成skywalking监控组件[附带log4j2集成]
  4. 论文解读GCN 1st《 Deep Embedding for CUnsupervisedlustering Analysis》
  5. 思维工具2: Reversal
  6. 史上最全CCA Spark and Hadoop Developer (CCA175) 开发者认证考试信息
  7. ERP 学习笔记 - 生产模块
  8. Vue基础——VueJS是什么、Vue的优缺点、vue2和vue3的模板区别、MVVM数据双向绑定、Vue的安装和使用、Vue模板语法-文本渲染、常用的vue的指令
  9. samba服务之samba-swat后台管理
  10. 安川机器人co文件_Robcad MOTOMAN 安川机器人数模