游戏类型,情节和风格的选择是一项非常有趣的任务,游戏的成功可能取决于这些问题的解决方案。此外,选择将基于其创建产品的技术也带来了自己的细微差别。我的目标是展示这个有趣过程的基本知识,因此,我将用一个简单的设计制作一个三维迷宫。而且,我将在不使用库和引擎的情况下以纯代码方式进行处理,例如three.js(尽管最好在其上进行大型项目)来展示如何根据需要创建引擎。一个完全自写的游戏可能是原创的,因此很有趣。通常,这两种方法各有利弊。
我想如果您正在阅读本文,那么您对为Google Chrome创建游戏这一主题感兴趣,这意味着您了解html-css-javaScript捆绑包的工作原理,因此我不会在基础上进行介绍,而是会立即开始开发。在所有现代浏览器都支持的html5和css3中(资源管理器不计算在内),可以在3维空间中排列块。还有一个元素,您可以在其中绘制线条和图形图元。大多数浏览器引擎使用<canvas>,因为可以在它上面做更多的事情,并且性能更好。但是对于简单的事情,完全可以使用transform-3d方法,它将花费更少的代码。
1.开发工具
我仅使用2种浏览器来检查网站和游戏:Chrome和Mozilla。所有其他浏览器(Explorer本身除外)都建立在第一个引擎上,因此我看不出使用它们的意义,因为结果与Chrome中的完全相同。记事本++足以编写代码。
2.如何在html中实现3D空间?
让我们看一下块坐标系:
默认情况下,子块的坐标(左和上)在x中为0像素,在y中为0像素。偏移(平移),在所有三个轴上均为0像素。让我们用一个示例来展示它,我们将为其创建一个新文件夹。在其中,我们将创建index.html,style.css和script.js文件。让我们打开index.html并在其中写入以下内容:
<!DOCTYPE HTML>
<HTML>
<HEAD>
<TITLE></TITLE>
<LINK rel="stylesheet" href="style.css">
<meta charset="utf-8">
</HEAD>
<BODY>
<div id="container">
<div id="world">
</div>
</div>
</BODY>
</HTML>
<script src="script.js"></script>
在style.css文件中,让我们设置“容器”和“世界”元素的样式。
#container{
position:absolute;
width:1200px;
height:800px;
border:2px solid #000000;
}
#world{
width:300px;
height:300px;
background-color:#C0FFFF;
}
让我们保存。使用Chrome打开index.html,我们得到:
让我们尝试将transform3d应用于元素“ world”:
#world{
width:300px;
height:300px;
background-color:#C0FFFF;
transform:translate3d(200px,100px,0px);
}
如您所知,我切换到全屏模式。现在让我们设置Z偏移量:
transform:translate3d(200px,100px,-1000px);
如果再次在浏览器中打开html文件,则不会看到任何更改。要查看更改,您需要为“容器”对象设置透视图:
#container{
position:absolute;
width:1200px;
height:800px;
border:2px solid #000000;
perspective:600px;
}
结果是:
广场已经远离我们。透视图在html中如何工作?让我们看一下图片:
d是用户到对象的距离,z是其坐标。负数z(在html中是translationZ)表示我们已移开对象,而正数z相反。透视值确定d的值。如果未设置Perspective属性,则将d值假定为无穷大,并且在这种情况下,用户不会随z的变化在视觉上改变对象。在我们的例子中,我们设置d = 600px。默认情况下,透视图视点位于元素的中心,但是,可以通过设置Perspective-origin:属性来更改它。
现在,让我们围绕某个轴旋转“世界”。在CSS中可以使用2种旋转方式。首先是绕x,y和z轴旋转。为此,请使用变换属性rotateX(),rotateY()和rotateZ()。第二个是使用rotate3d()属性绕给定轴旋转。我们将使用第一种方法,因为它更适合我们的任务。请注意,旋转轴从矩形的中心出来!
可以通过设置translate-origin:属性来更改发生转换的位置。因此,让我们设置“世界”沿x轴的旋转:
#world{
width:300px;
height:300px;
background-color:#C0FFFF;
transform:translate3d(200px,100px,0px) rotateX(45deg);
}
我们得到:
逆时针明显偏移。如果加上rotateY(),我们将沿Y轴获得一个偏移量,需要注意的是,当块旋转时,旋转轴也会旋转。您也可以尝试不同的旋转值。
现在,在“世界”块内,我们将创建另一个块,为此,我们向html文件添加了一个标记:
<!DOCTYPE HTML>
<HTML>
<HEAD>
<TITLE></TITLE>
<LINK rel="stylesheet" href="style.css">
<meta charset="utf-8">
</HEAD>
<BODY>
<div id="container">
<div id="world">
<div id="square1"></div>
</div>
</div>
</BODY>
</HTML>
<script src="script.js"></script>
在style.css中向此块添加样式:
#square1{
position:absolute;
width:200px;
height:200px;
background-color:#FF0000;
}
我们得到:
也就是说,“ world”块中的元素将作为该块的一部分进行转换。让我们尝试通过向其添加旋转样式来沿y轴旋转“ square1”:
transform:rotationY(30deg);
最后:
“轮换在哪里?” - 你问?实际上,这正是“ world”元素形成的平面上“ square1”块的投影的样子。但是我们不需要投影,而是真实的旋转。要使“世界”中的所有元素都具有三维效果,您需要对其应用transform-style:preserve-3d属性。将属性替换为“世界”样式列表后,检查更改:
优秀的!“正方形”块的一半隐藏在蓝色块的后面。为了完全显示它,请删除“ world”块的颜色,即,删除背景色行:#C0FFFF; 如果我们在“世界”块中添加更多矩形,则可以创建3D世界。现在,通过删除此元素样式中的transform属性行,来删除“世界”偏移量。
3.在三维世界中创建运动
为了使用户能够在这个世界中移动,您需要定义用于击键和鼠标移动的处理程序。控件将是标准的,这在大多数3D射击游戏中都存在。使用W,S,A,D键,我们将向前,向后,向左,向右移动,使用空格键将跳跃(换句话说,向上移动),并且使用鼠标将改变凝视的方向。为此,请打开script.js文件,该文件仍然为空。首先,让我们在其中添加以下变量:
// ?
var PressBack = 0;
var PressForward = 0;
var PressLeft = 0;
var PressRight = 0;
var PressUp = 0;
最初没有按下任何键。如果按下某个键,则某个变量的值将更改为1。如果释放它,它将变为0。我们将通过添加用于按下和释放键的处理程序来实现此目的:
//
document.addEventListener("keydown", (event) =>{
if (event.key == "a"){
PressLeft = 1;
}
if (event.key == "w"){
PressForward = 1;
}
if (event.key == "d"){
PressRight = 1;
}
if (event.key == "s"){
PressBack = 1;
}
if (event.keyCode == 32 && onGround){
PressUp = 1;
}
});
//
document.addEventListener("keyup", (event) =>{
if (event.key == "a"){
PressLeft = 0;
}
if (event.key == "w"){
PressForward = 0;
}
if (event.key == "d"){
PressRight = 0;
}
if (event.key == "s"){
PressBack = 0;
}
if (event.keyCode == 32){
PressUp = 0;
}
});
数字32是一个空格代码。如您所见,有一个onGround变量,它指示我们是否在地面上。现在,让我们通过在... ...变量之后添加onGround变量来允许向上移动:
// ?
var onGround = true;
因此,我们添加了一个推拉算法。现在我们需要添加机芯本身。实际上,我们正在前进。假设我们有一个正在移动的物体。我们称之为“典当”。按照普通开发人员的习惯,我们将为其创建一个单独的“ Player”类。奇怪的是,使用函数创建了javaScript中的类:
function player(x,y,z,rx,ry) {
this.x = x;
this.y = y;
this.z = z;
this.rx = rx;
this.ry = ry;
}
让我们将此代码粘贴到文件开头的script.js中。在文件末尾,让我们创建这种类型的对象:
//
var pawn = new player(0,0,0,0,0);
让我们写下这些变量的含义。x,y,z是玩家的初始坐标,rx,ry是其相对于x和y轴的旋转角度(以度为单位)。最后写的一行意味着我们创建了一个“玩家”类型的“典当”对象(我正在写一个具体的类型,而不是一个类,因为javascript中的类意味着其他一些事情),并且起始坐标为零。当我们移动对象时,世界坐标不应更改,而“典当”坐标应更改。这是在变量方面。从用户的角度来看,玩家在一个地方,但是世界在变化。因此,您需要强制程序更改播放器的坐标,处理这些更改并最终移动世界。实际上,这比听起来容易。
因此,在将文档加载到浏览器之后,我们将运行一个重绘世界的函数。让我们编写一个重绘函数:
function update(){
//
let dx = (PressRight - PressLeft);
let dz = - (PressForward - PressBack);
let dy = PressUp;
//
pawn.x = pawn.x + dx;
pawn.y = pawn.y + dy;
pawn.z = pawn.z + dz;
// ( )
world.style.transform =
"rotateX(" + (-pawn.rx) + "deg)" +
"rotateY(" + (-pawn.ry) + "deg)" +
"translate3d(" + (-pawn.x) + "px," + (-pawn.y) + "px," + (-pawn.z) + "px)";
};
在新的浏览器中,world将匹配id =“ world”的元素,但是使用以下结构在update()函数之前进行分配是更安全的:
var world = document.getElementById("world");
我们将每10毫秒(每秒更新100次)更改世界的位置,为此我们将开始一个无限循环:
TimerGame = setInterval(update,10);
让我们开始游戏。欢呼,现在我们可以行动了!但是,世界超出了“容器”元素的范围。为了防止这种情况的发生,我们在style.css中为其设置一个css属性。添加行溢出:隐藏;并查看更改。现在,世界仍在容器内。
您可能并不总是了解需要在何处编写某些代码行,因此现在我将向您提供一些文件,我相信您应该得到:
index.html:
<!DOCTYPE HTML>
<HTML>
<HEAD>
<TITLE></TITLE>
<LINK rel="stylesheet" href="style.css">
<meta charset="utf-8">
</HEAD>
<BODY>
<div id="container">
<div id="world">
<div id="square1"></div>
</div>
</div>
</BODY>
</HTML>
<script src="script.js"></script>
style.css:
#container{
position:absolute;
width:1200px;
height:800px;
border:2px solid #000000;
perspective:600px;
overflow:hidden;
}
#world{
position:absolute;
width:300px;
height:300px;
transform-style:preserve-3d;
}
#square1{
position:absolute;
width:200px;
height:200px;
background-color:#FF0000;
transform:rotateY(30deg);
}
script.js:
// Pawn
function player(x,y,z,rx,ry) {
this.x = x;
this.y = y;
this.z = z;
this.rx = rx;
this.ry = ry;
}
// ?
var PressBack = 0;
var PressForward = 0;
var PressLeft = 0;
var PressRight = 0;
var PressUp = 0;
// ?
var onGround = true;
//
document.addEventListener("keydown", (event) =>{
if (event.key == "a"){
PressLeft = 1;
}
if (event.key == "w"){
PressForward = 1;
}
if (event.key == "d"){
PressRight = 1;
}
if (event.key == "s"){
PressBack = 1;
}
if (event.keyCode == 32 && onGround){
PressUp = 1;
}
});
//
document.addEventListener("keyup", (event) =>{
if (event.key == "a"){
PressLeft = 0;
}
if (event.key == "w"){
PressForward = 0;
}
if (event.key == "d"){
PressRight = 0;
}
if (event.key == "s"){
PressBack = 0;
}
if (event.keyCode == 32){
PressUp = 0;
}
});
//
var pawn = new player(0,0,0,0,0);
// world
var world = document.getElementById("world");
function update(){
//
let dx = (PressRight - PressLeft);
let dz = - (PressForward - PressBack);
let dy = - PressUp;
//
pawn.x = pawn.x + dx;
pawn.y = pawn.y + dy;
pawn.z = pawn.z + dz;
// ( )
world.style.transform =
"rotateX(" + (-pawn.rx) + "deg)" +
"rotateY(" + (-pawn.ry) + "deg)" +
"translate3d(" + (-pawn.x) + "px," + (-pawn.y) + "px," + (-pawn.z) + "px)";
};
TimerGame = setInterval(update,10);
如果您有其他不同之处,请务必更正!
我们学习了如何移动角色,但还不知道如何旋转角色!当然,字符的旋转将由鼠标完成。对于鼠标,在按...键的状态变量中,我们添加鼠标移动的状态变量:
// ?
var PressBack = 0;
var PressForward = 0;
var PressLeft = 0;
var PressRight = 0;
var PressUp = 0;
var MouseX = 0;
var MouseY = 0;
在推释放处理程序之后,插入运动处理程序:
//
document.addEventListener("mousemove", (event)=>{
MouseX = event.movementX;
MouseY = event.movementY;
});
向更新功能添加旋转:
//
let dx = (PressRight - PressLeft);
let dz = - (PressForward - PressBack);
let dy = - PressUp;
let drx = MouseY;
let dry = - MouseX;
//
pawn.x = pawn.x + dx;
pawn.y = pawn.y + dy;
pawn.z = pawn.z + dz;
pawn.rx = pawn.rx + drx;
pawn.ry = pawn.ry + dry;
请注意,沿y轴移动鼠标会沿x轴旋转棋子,反之亦然。如果我们看结果,我们将为所见所惊。关键是,如果没有偏移量,则MouseX和MouseY保持不变,并且不等于零。这意味着在每次更新迭代之后,Misha的偏移量应重置为零:
//
let dx = (PressRight - PressLeft);
let dz = - (PressForward - PressBack);
let dy = - PressUp;
let drx = MouseY;
let dry = - MouseX;
// :
MouseX = MouseY = 0;
//
pawn.x = pawn.x + dx;
pawn.y = pawn.y + dy;
pawn.z = pawn.z + dz;
pawn.rx = pawn.rx + drx;
pawn.ry = pawn.ry + dry;
更好的是,我们摆脱了旋转惯性,但是旋转仍然很奇怪!要了解发生了什么,让我们在“容器”中添加“ pawn” div:
<div id="container">
<div id="world">
<div id="square1"></div>
</div>
<div id="pawn"></div>
</div>
让我们在style.css中设置样式:
#pawn{
position:absolute;
width:100px;
height:100px;
top:400px;
left:600px;
transform:translate(-50%,-50%);
background-color:#0000FF;
}
让我们检查结果。现在一切都顺利了!唯一的是,蓝色方块仍保留在前面,但现在让我们离开。要使游戏成为第一人称视角而不是第三人称视角,您需要以远景价值使世界更接近我们。让我们在update。)函数的script.js中进行操作:
world.style.transform =
"translateZ(600px)" +
"rotateX(" + (-pawn.rx) + "deg)" +
"rotateY(" + (-pawn.ry) + "deg)" +
"translate3d(" + (-pawn.x) + "px," + (-pawn.y) + "px," + (-pawn.z) + "px)";
现在,您可以从第一人称视角制作游戏。通过在style.css中添加一行来隐藏典当:
#pawn{
display:none;
position:absolute;
top:400px;
left:600px;
width:100px;
height:100px;
transform:translate(-50%,-50%);
background-color:#0000FF;
}
优秀的。我必须立即说,在一个只有一个正方形的世界中导航非常困难,因此我们将创建一个站点。让我们将“ square2”块添加到“世界”中:
<div id="world">
<div id="square1"></div>
<div id="square2"></div>
</div>
然后在style.css中为其添加样式:
#square2{
position:absolute;
width:1000px;
height:1000px;
top:400px;
left:600px;
background-color:#00FF00;
transform:translate(-50%,-50%) rotateX(90deg) translateZ(-100px);
}
现在一切都清楚了。好吧,不完全是。当我们按下键时,我们严格沿X和Z轴移动,并且我们希望沿视图方向移动。让我们执行以下操作:在script.js文件的开头,添加2个变量:
//
var pi = 3.141592;
var deg = pi/180;
度是pi /弧度180。我们将不得不应用从弧度计算的正弦和余弦。应该做什么?看一下图片:
当我们的视线指向某个角度并且想要向前移动时,两个坐标都将发生变化:X和Z.如果我们移到侧面,则三角函数将简单地交换位置,并且生成的正弦前面的符号将发生变化。让我们在update()中更改偏移方程:
//
let dx = (PressRight - PressLeft)*Math.cos(pawn.ry*deg) - (PressForward - PressBack)*Math.sin(pawn.ry*deg);
let dz = - (PressForward - PressBack)*Math.cos(pawn.ry*deg) - (PressRight - PressLeft)*Math.sin(pawn.ry*deg);
let dy = -PressUp;
let drx = MouseY;
let dry = - MouseX;
仔细检查所有文件!如果您发现某些问题是错误的,那么肯定会有错误使您伤脑筋!
index.html:
<!DOCTYPE HTML>
<HTML>
<HEAD>
<TITLE></TITLE>
<LINK rel="stylesheet" href="style.css">
<meta charset="utf-8">
</HEAD>
<BODY>
<div id="container">
<div id="world">
<div id="square1"></div>
<div id="square2"></div>
</div>
<div id="pawn"></div>
</div>
</BODY>
</HTML>
<script src="script.js"></script>
style.css:
#container{
position:absolute;
width:1200px;
height:800px;
border:2px solid #000000;
perspective:600px;
overflow:hidden;
}
#world{
position:absolute;
width:inherit;
height:inherit;
transform-style:preserve-3d;
}
#square1{
position:absolute;
width:200px;
height:200px;
top:400px;
left:600px;
background-color:#FF0000;
transform:translate(-50%,-50%) rotateY(30deg);
}
#square2{
position:absolute;
width:1000px;
height:1000px;
top:400px;
left:600px;
background-color:#00FF00;
transform:translate(-50%,-50%) rotateX(90deg) translateZ(-100px);
}
#pawn{
display:none;
position:absolute;
top:400px;
left:600px;
transform:translate(-50%,-50%);
width:100px;
height:100px;
background-color:#0000FF;
}
script.js:
//
var pi = 3.141592;
var deg = pi/180;
// Pawn
function player(x,y,z,rx,ry) {
this.x = x;
this.y = y;
this.z = z;
this.rx = rx;
this.ry = ry;
}
// ?
var PressBack = 0;
var PressForward = 0;
var PressLeft = 0;
var PressRight = 0;
var PressUp = 0;
var MouseX = 0;
var MouseY = 0;
// ?
var onGround = true;
//
document.addEventListener("keydown", (event) =>{
if (event.key == "a"){
PressLeft = 1;
}
if (event.key == "w"){
PressForward = 1;
}
if (event.key == "d"){
PressRight = 1;
}
if (event.key == "s"){
PressBack = 1;
}
if (event.keyCode == 32 && onGround){
PressUp = 1;
}
});
//
document.addEventListener("keyup", (event) =>{
if (event.key == "a"){
PressLeft = 0;
}
if (event.key == "w"){
PressForward = 0;
}
if (event.key == "d"){
PressRight = 0;
}
if (event.key == "s"){
PressBack = 0;
}
if (event.keyCode == 32){
PressUp = 0;
}
});
//
document.addEventListener("mousemove", (event)=>{
MouseX = event.movementX;
MouseY = event.movementY;
});
// player
var pawn = new player(0,0,0,0,0);
// world
var world = document.getElementById("world");
function update(){
//
let dx = (PressRight - PressLeft)*Math.cos(pawn.ry*deg) - (PressForward - PressBack)*Math.sin(pawn.ry*deg);
let dz = - (PressForward - PressBack)*Math.cos(pawn.ry*deg) - (PressRight - PressLeft)*Math.sin(pawn.ry*deg);
let dy = - PressUp;
let drx = MouseY;
let dry = - MouseX;
// :
MouseX = MouseY = 0;
//
pawn.x = pawn.x + dx;
pawn.y = pawn.y + dy;
pawn.z = pawn.z + dz;
pawn.rx = pawn.rx + drx;
pawn.ry = pawn.ry + dry;
// ( )
world.style.transform =
"translateZ(600px)" +
"rotateX(" + (-pawn.rx) + "deg)" +
"rotateY(" + (-pawn.ry) + "deg)" +
"translate3d(" + (-pawn.x) + "px," + (-pawn.y) + "px," + (-pawn.z) + "px)";
};
TimerGame = setInterval(update,10);
我们几乎想通了运动。但是有一个不便之处:鼠标光标只能在屏幕内移动。在三维射击游戏中,您可以将鼠标旋转尽可能长的距离。我们也做一下:当我们单击游戏屏幕(在“容器”上)时,光标将消失,并且我们将能够在不限制屏幕大小的情况下旋转鼠标。我们在单击屏幕时激活鼠标捕获,为此我们在按键处理程序前面的“容器”上放置了一个用于单击鼠标的处理程序:
// container
var container = document.getElementById("container");
//
container.onclick = function(){
container.requestPointerLock();
};
现在是另一回事了。但是,通常最好只在捕获光标时进行旋转。让我们在新闻发布后介绍一个新变量...
// ?
var lock = false;
让我们在光标捕获处理程序之前添加一个用于更改光标捕获状态(捕获或未捕获)的处理程序(抱歉,重言式):
//
document.addEventListener("pointerlockchange", (event)=>{
lock = !lock;
});
并在update()中添加“ pawn”旋转条件:
// ,
if (lock){
pawn.rx = pawn.rx + drx;
pawn.ry = pawn.ry + dry;
};
并且仅当尚未捕获光标时才允许在单击容器时捕获鼠标本身:
//
container.onclick = function(){
if (!lock) container.requestPointerLock();
};
我们已经完全解决了这一运动。让我们继续创造世界
4.加载地图
在我们的情况下,最方便地将世界表示为一组具有不同位置,旋转,大小和颜色的矩形。也可以使用纹理代替颜色。实际上,游戏中所有现代3D世界都是称为多边形的三角形和矩形的集合。在很酷的游戏中,仅在一帧中它们的数量就可以达到数万个。由于浏览器本身的图形性能很低,我们将有大约一百个。在前面的段落中,我们在“世界”中插入了“ div”块。但是,如果有很多这样的块(数百个),那么将每个块插入容器中将非常繁琐。并且可以有很多级别。因此,让javaScript插入这些矩形,而不是我们。我们将为其创建一个特殊的数组。
让我们打开index.html并从“ world”块中删除所有内部块:
<BODY>
<div id="container">
<div id="world"></div>
<div id="pawn"></div>
</div>
</BODY>
如您所见,“世界”现在什么都没有了。在style.css中,删除#square1和#square2的样式(从此文件中完全删除#square1和#square2),然后为.square类创建样式,这对于所有矩形都是相同的。我们将为此设置一个属性:
.square{
position:absolute;
}
现在,让我们创建一个矩形数组(例如,我们将其推入script.js中的播放器构造函数和press变量之间):
//
var map = [
[0,0,1000,0,180,0,2000,200,"#F0C0FF"],
[0,0,-1000,0,0,0,2000,200,"#F0C0FF"],
[1000,0,0,0,-90,0,2000,200,"#F0C0FF"],
[-1000,0,0,0,90,0,2000,200,"#F0C0FF"],
[0,100,0,90,0,0,2000,2000,"#666666"]
]
可以以构造函数的形式进行此操作,但是现在我们将使用纯数组进行管理,因为开始通过数组而不是通过构造函数放置矩形的周期更容易开始。我将解释其中的数字含义。映射数组包含9个变量的一维数组:[,,,,,,,,]。我想您知道,前三个数字是矩形中心的坐标,后三个数字是度数的旋转角度(相对于同一中心),然后两个数字是其尺寸,最后一个数字是背景。此外,背景可以是纯色,渐变或照片。后者非常方便用作纹理。
我们已经编写了数组,现在将编写一个函数,将数组转换为实际的矩形:
function CreateNewWorld(){
for (let i = 0; i < map.length; i++){
//
let newElement = document.createElement("div");
newElement.className = "square";
newElement.id = "square" + i;
newElement.style.width = map[i][6] + "px";
newElement.style.height = map[i][7] + "px";
newElement.style.background = map[i][8];
newElement.style.transform = "translate3d(" +
(600 - map[i][6]/2 + map[i][0]) + "px," +
(400 - map[i][7]/2 + map[i][1]) + "px," +
(map[i][2]) + "px)" +
"rotateX(" + map[i][3] + "deg)" +
"rotateY(" + map[i][4] + "deg)" +
"rotateZ(" + map[i][5] + "deg)";
// world
world.append(newElement);
}
}
让我解释发生了什么:我们正在创建一个新变量,该变量指向刚创建的元素。我们给它分配一个id和一个css类(这是javaScript语言中的单词class的意思),并设置高度,背景和转换的宽度。值得注意的是,在变换中,除了矩形中心的坐标外,我们还指定了600和400的偏移量以及一半的尺寸,以便矩形的中心恰好位于具有所需坐标的点上。让我们在计时器之前启动世界生成器:
CreateNewWorld();
TimerGame = setInterval(update,10);
现在,我们看到了一个粉红色墙壁和灰色地板的区域。如您所见,创建地图在技术上并不困难。结果,三个文件中的代码应如下所示:
index.html:
<!DOCTYPE HTML>
<HTML>
<HEAD>
<TITLE></TITLE>
<LINK rel="stylesheet" href="style.css">
<meta charset="utf-8">
</HEAD>
<BODY>
<div id="container">
<div id="world"></div>
<div id="pawn"></div>
</div>
</BODY>
</HTML>
<script src="script.js"></script>
style.css
#container{
position:absolute;
width:1200px;
height:800px;
border:2px solid #000000;
perspective:600px;
overflow:hidden;
}
#world{
position:absolute;
width:inherit;
height:inherit;
transform-style:preserve-3d;
}
.square{
position:absolute;
}
#pawn{
display:none;
position:absolute;
top:400px;
left:600px;
transform:translate(-50%,-50%);
width:100px;
height:100px;
}
script.js:
//
var pi = 3.141592;
var deg = pi/180;
// player
function player(x,y,z,rx,ry) {
this.x = x;
this.y = y;
this.z = z;
this.rx = rx;
this.ry = ry;
}
//
var map = [
[0,0,1000,0,180,0,2000,200,"#F0C0FF"],
[0,0,-1000,0,0,0,2000,200,"#F0C0FF"],
[1000,0,0,0,-90,0,2000,200,"#F0C0FF"],
[-1000,0,0,0,90,0,2000,200,"#F0C0FF"],
[0,100,0,90,0,0,2000,2000,"#666666"]
]
// ?
var PressBack = 0;
var PressForward = 0;
var PressLeft = 0;
var PressRight = 0;
var PressUp = 0;
var MouseX = 0;
var MouseY = 0;
// ?
var lock = false;
// ?
var onGround = true;
// container
var container = document.getElementById("container");
//
document.addEventListener("pointerlockchange", (event)=>{
lock = !lock;
});
//
container.onclick = function(){
if (!lock) container.requestPointerLock();
};
//
document.addEventListener("keydown", (event) =>{
if (event.key == "a"){
PressLeft = 1;
}
if (event.key == "w"){
PressForward = 1;
}
if (event.key == "d"){
PressRight = 1;
}
if (event.key == "s"){
PressBack = 1;
}
if (event.keyCode == 32 && onGround){
PressUp = 1;
}
});
//
document.addEventListener("keyup", (event) =>{
if (event.key == "a"){
PressLeft = 0;
}
if (event.key == "w"){
PressForward = 0;
}
if (event.key == "d"){
PressRight = 0;
}
if (event.key == "s"){
PressBack = 0;
}
if (event.keyCode == 32){
PressUp = 0;
}
});
//
document.addEventListener("mousemove", (event)=>{
MouseX = event.movementX;
MouseY = event.movementY;
});
//
var pawn = new player(0,0,0,0,0);
// world
var world = document.getElementById("world");
function update(){
//
let dx = (PressRight - PressLeft)*Math.cos(pawn.ry*deg) - (PressForward - PressBack)*Math.sin(pawn.ry*deg);
let dz = - (PressForward - PressBack)*Math.cos(pawn.ry*deg) - (PressRight - PressLeft)*Math.sin(pawn.ry*deg);
let dy = - PressUp;
let drx = MouseY;
let dry = - MouseX;
// :
MouseX = MouseY = 0;
//
pawn.x = pawn.x + dx;
pawn.y = pawn.y + dy;
pawn.z = pawn.z + dz;
// ,
if (lock){
pawn.rx = pawn.rx + drx;
pawn.ry = pawn.ry + dry;
};
// ( )
world.style.transform =
"translateZ(" + (600 - 0) + "px)" +
"rotateX(" + (-pawn.rx) + "deg)" +
"rotateY(" + (-pawn.ry) + "deg)" +
"translate3d(" + (-pawn.x) + "px," + (-pawn.y) + "px," + (-pawn.z) + "px)";
};
function CreateNewWorld(){
for (let i = 0; i < map.length; i++){
//
let newElement = document.createElement("div");
newElement.className = "square";
newElement.id = "square" + i;
newElement.style.width = map[i][6] + "px";
newElement.style.height = map[i][7] + "px";
newElement.style.background = map[i][8];
newElement.style.transform = "translate3d(" +
(600 - map[i][6]/2 + map[i][0]) + "px," +
(400 - map[i][7]/2 + map[i][1]) + "px," +
(map[i][2]) + "px)" +
"rotateX(" + map[i][3] + "deg)" +
"rotateY(" + map[i][4] + "deg)" +
"rotateZ(" + map[i][5] + "deg)";
// world
world.append(newElement);
}
}
CreateNewWorld();
TimerGame = setInterval(update,10);
如果一切顺利,请继续进行下一项。
5.玩家与世界物体的碰撞
我们创建了一种运动技术,即通过数组产生世界的生成器。我们可以环游一个美丽的世界。但是,我们的玩家尚未与他互动。为了进行这种交互,我们需要检查玩家是否与任何矩形发生碰撞?也就是说,我们将检查碰撞。首先,让我们插入一个空函数:
function collision(){
}
我们将其称为update():
// :
MouseX = MouseY = 0;
//
collision();
这是怎么发生的?假设玩家是半径为r的球。并且它朝着矩形移动:
显然,如果从球到矩形平面的距离大于r,则肯定不会发生碰撞。要找出此距离,可以将播放器的坐标转换为矩形坐标系。让我们编写从世界系统转移到矩形系统的功能:
function coorTransform(x0,y0,z0,rxc,ryc,rzc){
let x1 = x0;
let y1 = y0*Math.cos(rxc*deg) + z0*Math.sin(rxc*deg);
let z1 = -y0*Math.sin(rxc*deg) + z0*Math.cos(rxc*deg);
let x2 = x1*Math.cos(ryc*deg) - z1*Math.sin(ryc*deg);
let y2 = y1;
let z2 = x1*Math.sin(ryc*deg) + z1*Math.cos(ryc*deg);
let x3 = x2*Math.cos(rzc*deg) + y2*Math.sin(rzc*deg);
let y3 = -x2*Math.sin(rzc*deg) + y2*Math.cos(rzc*deg);
let z3 = z2;
return [x3,y3,z3];
}
和反函数:
function coorReTransform (x3,y3,z3,rxc,ryc,rzc){
let x2 = x3*Math.cos(rzc*deg) - y3*Math.sin(rzc*deg);
let y2 = x3*Math.sin(rzc*deg) + y3*Math.cos(rzc*deg);
let z2 = z3
let x1 = x2*Math.cos(ryc*deg) + z2*Math.sin(ryc*deg);
let y1 = y2;
let z1 = -x2*Math.sin(ryc*deg) + z2*Math.cos(ryc*deg);
let x0 = x1;
let y0 = y1*Math.cos(rxc*deg) - z1*Math.sin(rxc*deg);
let z0 = y1*Math.sin(rxc*deg) + z1*Math.cos(rxc*deg);
return [x0,y0,z0];
}
让我们在update()函数之后插入这些函数。我不会解释它是如何工作的,因为我不想上分析几何课程。我会说有这样的公式可以在旋转过程中平移坐标,我们只是使用它们。从矩形的角度来看,我们的播放器的位置如下:
在这种情况下,碰撞条件如下:如果将球移位值v(v是向量)后,z坐标位于–r和r之间,并且x和y坐标位于矩形内或与矩形相距不大于r,则发生冲突。在这种情况下,移动后玩家的z坐标将为r或-r(取决于玩家来自哪一侧)。因此,玩家的偏移被改变。我们专门在()更新玩家坐标以更改时间偏移之前调用碰撞。因此,球将永远不会像其他碰撞算法中那样与矩形相交。尽管从物理上讲,玩家将更有可能成为一个立方体,但我们不会对此予以关注。因此,让我们在javaScript中实现它:
function collision(){
for(let i = 0; i < map.length; i++){
//
let x0 = (pawn.x - map[i][0]);
let y0 = (pawn.y - map[i][1]);
let z0 = (pawn.z - map[i][2]);
let x1 = x0 + dx;
let y1 = y0 + dy;
let z1 = z0 + dz;
let point0 = coorTransform(x0,y0,z0,map[i][3],map[i][4],map[i][5]);
let point1 = coorTransform(x1,y1,z1,map[i][3],map[i][4],map[i][5]);
let point2 = new Array();
//
if (Math.abs(point1[0])<(map[i][6]+98)/2 && Math.abs(point1[1])<(map[i][7]+98)/2 && Math.abs(point1[2]) < 50){
point1[2] = Math.sign(point0[2])*50;
point2 = coorReTransform(point1[0],point1[1],point1[2],map[i][3],map[i][4],map[i][5]);
dx = point2[0] - x0;
dy = point2[1] - y0;
dz = point2[2] - z0;
}
};
}
x0,y0和z0是矩形坐标系中玩家的初始坐标(无旋转.x1,y1和z1是无碰撞后玩家的坐标.point0,point0,point1和point2是初始半径矢量,无位移后的半径矢量映射[i] [3]和其他(如果您还记得的话)是矩形的旋转角度,请注意,在这种情况下,矩形的大小不是100,而是98,这是拐杖,这是为什么呢?开始游戏,您应该会看到一些相当高质量的碰撞。
如您所见,所有这些操作都在所有矩形的for循环中发生。由于有很多对坐标转换函数的调用,因此它们的数量很多,因此这种操作变得非常昂贵,因为它们还执行许多数学运算。显然,如果矩形距离播放器很远,那么计算碰撞就没有意义了。让我们添加以下条件:
if ((x0**2 + y0**2 + z0**2 + dx**2 + dy**2 + dz**2) < (map[i][1]**2 + map[i][2]**2)){
let x1 = x0 + dx;
let y1 = y0 + dy;
let z1 = z0 + dz;
let point0 = coorTransform(x0,y0,z0,map[i][3],map[i][4],map[i][5]);
let point1 = coorTransform(x1,y1,z1,map[i][3],map[i][4],map[i][5]);
let point2 = new Array();
//
if (Math.abs(point1[0])<(map[i][6]+98)/2 && Math.abs(point1[1])<(map[i][7]+98)/2 && Math.abs(point1[2]) < 50){
point1[2] = Math.sign(point0[2])*50;
point2 = coorReTransform(point1[0],point1[1],point1[2],map[i][3],map[i][4],map[i][5]);
dx = point2[0] - x0;
dy = point2[1] - y0;
dz = point2[2] - z0;
}
}
因此,我们处理了碰撞。我们可以轻松地在倾斜的表面上攀爬,并且,如果可能的话,只有在慢速的系统上才可能发生错误。实际上,整个主要技术部分到此结束。我们只需要添加私人物品,例如重力,物品,菜单,声音,精美的图形。但这很容易做到,并且与我们刚刚制造的引擎无关。因此,我将在下一部分中对此进行讨论。现在检查一下我的代码:
index.html:
<!DOCTYPE HTML>
<HTML>
<HEAD>
<TITLE></TITLE>
<LINK rel="stylesheet" href="style.css">
<meta charset="utf-8">
</HEAD>
<BODY>
<div id="container">
<div id="world"></div>
<div id="pawn"></div>
</div>
</BODY>
</HTML>
<script src="script.js"></script>
style.css
#container{
position:absolute;
width:1200px;
height:800px;
border:2px solid #000000;
perspective:600px;
overflow:hidden;
}
#world{
position:absolute;
width:inherit;
height:inherit;
transform-style:preserve-3d;
}
.square{
position:absolute;
}
#pawn{
display:none;
position:absolute;
top:400px;
left:600px;
transform:translate(-50%,-50%);
width:100px;
height:100px;
}
script.js:
//
var pi = 3.141592;
var deg = pi/180;
// player
function player(x,y,z,rx,ry) {
this.x = x;
this.y = y;
this.z = z;
this.rx = rx;
this.ry = ry;
}
//
var map = [
[0,0,1000,0,180,0,2000,200,"#F0C0FF"],
[0,0,-1000,0,0,0,2000,200,"#F0C0FF"],
[1000,0,0,0,-90,0,2000,200,"#F0C0FF"],
[-1000,0,0,0,90,0,2000,200,"#F0C0FF"],
[0,100,0,90,0,0,2000,2000,"#666666"]
];
// ?
var PressBack = 0;
var PressForward = 0;
var PressLeft = 0;
var PressRight = 0;
var PressUp = 0;
var MouseX = 0;
var MouseY = 0;
// ?
var lock = false;
// ?
var onGround = true;
// container
var container = document.getElementById("container");
//
document.addEventListener("pointerlockchange", (event)=>{
lock = !lock;
});
//
container.onclick = function(){
if (!lock) container.requestPointerLock();
};
//
document.addEventListener("keydown", (event) =>{
if (event.key == "a"){
PressLeft = 1;
}
if (event.key == "w"){
PressForward = 1;
}
if (event.key == "d"){
PressRight = 1;
}
if (event.key == "s"){
PressBack = 1;
}
if (event.keyCode == 32 && onGround){
PressUp = 1;
}
});
//
document.addEventListener("keyup", (event) =>{
if (event.key == "a"){
PressLeft = 0;
}
if (event.key == "w"){
PressForward = 0;
}
if (event.key == "d"){
PressRight = 0;
}
if (event.key == "s"){
PressBack = 0;
}
if (event.keyCode == 32){
PressUp = 0;
}
});
//
document.addEventListener("mousemove", (event)=>{
MouseX = event.movementX;
MouseY = event.movementY;
});
//
var pawn = new player(-900,0,-900,0,0);
// world
var world = document.getElementById("world");
function update(){
//
dx = (PressRight - PressLeft)*Math.cos(pawn.ry*deg) - (PressForward - PressBack)*Math.sin(pawn.ry*deg);
dz = - (PressForward - PressBack)*Math.cos(pawn.ry*deg) - (PressRight - PressLeft)*Math.sin(pawn.ry*deg);
dy = - PressUp;
drx = MouseY;
dry = - MouseX;
// :
MouseX = MouseY = 0;
//
collision();
//
pawn.x = pawn.x + dx;
pawn.y = pawn.y + dy;
pawn.z = pawn.z + dz;
console.log(pawn.x + ":" + pawn.y + ":" + pawn.z);
// ,
if (lock){
pawn.rx = pawn.rx + drx;
pawn.ry = pawn.ry + dry;
};
// ( )
world.style.transform =
"translateZ(" + (600 - 0) + "px)" +
"rotateX(" + (-pawn.rx) + "deg)" +
"rotateY(" + (-pawn.ry) + "deg)" +
"translate3d(" + (-pawn.x) + "px," + (-pawn.y) + "px," + (-pawn.z) + "px)";
};
function CreateNewWorld(){
for (let i = 0; i < map.length; i++){
//
let newElement = document.createElement("div");
newElement.className = "square";
newElement.id = "square" + i;
newElement.style.width = map[i][6] + "px";
newElement.style.height = map[i][7] + "px";
newElement.style.background = map[i][8];
newElement.style.transform = "translate3d(" +
(600 - map[i][6]/2 + map[i][0]) + "px," +
(400 - map[i][7]/2 + map[i][1]) + "px," +
(map[i][2]) + "px)" +
"rotateX(" + map[i][3] + "deg)" +
"rotateY(" + map[i][4] + "deg)" +
"rotateZ(" + map[i][5] + "deg)";
// world
world.append(newElement);
}
}
function collision(){
for(let i = 0; i < map.length; i++){
//
let x0 = (pawn.x - map[i][0]);
let y0 = (pawn.y - map[i][1]);
let z0 = (pawn.z - map[i][2]);
if ((x0**2 + y0**2 + z0**2 + dx**2 + dy**2 + dz**2) < (map[i][6]**2 + map[i][7]**2)){
let x1 = x0 + dx;
let y1 = y0 + dy;
let z1 = z0 + dz;
let point0 = coorTransform(x0,y0,z0,map[i][3],map[i][4],map[i][5]);
let point1 = coorTransform(x1,y1,z1,map[i][3],map[i][4],map[i][5]);
let point2 = new Array();
//
if (Math.abs(point1[0])<(map[i][6]+98)/2 && Math.abs(point1[1])<(map[i][7]+98)/2 && Math.abs(point1[2]) < 50){
point1[2] = Math.sign(point0[2])*50;
point2 = coorReTransform(point1[0],point1[1],point1[2],map[i][3],map[i][4],map[i][5]);
dx = point2[0] - x0;
dy = point2[1] - y0;
dz = point2[2] - z0;
}
}
};
}
function coorTransform(x0,y0,z0,rxc,ryc,rzc){
let x1 = x0;
let y1 = y0*Math.cos(rxc*deg) + z0*Math.sin(rxc*deg);
let z1 = -y0*Math.sin(rxc*deg) + z0*Math.cos(rxc*deg);
let x2 = x1*Math.cos(ryc*deg) - z1*Math.sin(ryc*deg);
let y2 = y1;
let z2 = x1*Math.sin(ryc*deg) + z1*Math.cos(ryc*deg);
let x3 = x2*Math.cos(rzc*deg) + y2*Math.sin(rzc*deg);
let y3 = -x2*Math.sin(rzc*deg) + y2*Math.cos(rzc*deg);
let z3 = z2;
return [x3,y3,z3];
}
function coorReTransform(x3,y3,z3,rxc,ryc,rzc){
let x2 = x3*Math.cos(rzc*deg) - y3*Math.sin(rzc*deg);
let y2 = x3*Math.sin(rzc*deg) + y3*Math.cos(rzc*deg);
let z2 = z3
let x1 = x2*Math.cos(ryc*deg) + z2*Math.sin(ryc*deg);
let y1 = y2;
let z1 = -x2*Math.sin(ryc*deg) + z2*Math.cos(ryc*deg);
let x0 = x1;
let y0 = y1*Math.cos(rxc*deg) - z1*Math.sin(rxc*deg);
let z0 = y1*Math.sin(rxc*deg) + z1*Math.cos(rxc*deg);
return [x0,y0,z0];
}
CreateNewWorld();
TimerGame = setInterval(update,10);