从头开始使用纯HTML,CSS和JS创建基于浏览器的3D游戏。第1/2部分

现代计算技术可让您创建出色的计算机游戏!现在,具有3d图形的游戏非常受欢迎,因为玩这些游戏后,您会陷入虚构的世界并失去与现实的一切联系。互联网和浏览器技术的发展使得您无需下载就可以在您喜欢的Chrome,Mozilla或其他工具(对Explorer保持安静)中运行拼图和射击游戏。因此,在这里我将告诉您如何创建一个简单的三维浏览器游戏。



游戏类型,情节和风格的选择是一项非常有趣的任务,游戏的成功可能取决于这些问题的解决方案。此外,选择将基于其创建产品的技术也带来了自己的细微差别。我的目标是展示这个有趣过程的基本知识,因此,我将用一个简单的设计制作一个三维迷宫。而且,我将在不使用库和引擎的情况下以纯代码方式进行处理,例如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);



All Articles