我正在创建一个名为spindizzy的 atari 8bit 游戏作为我的学校项目的重新制作。
我想出了如何使用3d points
转换为2d perspective point
来渲染它。
我也可以旋转形状(渲染顺序的小问题)。
我的目标是这样的:
Vector3D -> Vector2D
- 已完成
Block -> [[Vector3D]] -> [[Vector2D]] -> [Shape]
- 已完成
Map -> [Location] -> [[[Block]]]
但是我不知道应该对什么进行检测,以及如何对其进行检测。
我的目标语言是js
,但如果给出的答案是其他语言或只是描述如何解决此问题,则无关紧要。
不允许使用框架。
我正在使用画布的2d
上下文。
Here您可以看到我的代码。
我会对这些链接和建议表示感谢。
答案 0 :(得分:2)
忽略投影并仅在具有某些假设的3D空间中工作。
每个块由一个对象定义,该对象保持位置(x,y,z),角高,类型(split1,split2或quad)和表面norm / s
单个函数将返回块上x,y处的点的高度。块pNorm属性将在该点设置为曲面法线。 (不要修改法线。如果需要修改它,请先创建副本)
表面法线是垂直于平面的线。进行高度测试时,属性block.pNorm
设置为适当的法线。法线用于确定球应该滚动的方向。 (我没有包括任何z运动,球被粘在表面上)。法线也用于确定阴影,以及球反弹的方向。
解释的最佳方式是通过演示。有很多代码可以让演示发生,所以请问你是否有任何问题。
注意代码是用一点ES6编写的,因此需要babel才能在旧版浏览器上运行。
更新第一篇文章我有一个我没有发现的错误(法线错误设置)。我现在修好了。我还添加了一个错误,如果地图包含无效的块,将抛出RangeError。
var canvas = document.createElement("canvas");
canvas.width = 500;
canvas.height = 300;
var ctx = canvas.getContext("2d");
document.body.appendChild(canvas);
// block types
const types = {
quad : 1,
split1 : 2, // split from top left to bottom right
split2 : 3, // split from top right to bottom left
}
/*
// A block object example to define meaning of properties
var blockObject = {
x : 0, // top left base x pos
y : 0, // top left base y pos
z : 0, // top left base z pos
norm1, // normal of quad or top right or bottom right triangles
norm2, // normal of quad or top left or bottom left triangles
p1 : 0, // top left
p2 : 0, // top right
p3 : 0, // bottom right
p4 : 0, // bottom left
type : types.quad,
pNorm : null, // this is set when a height test is done. It is the normal at the point of the height test
}*/
// compute the surface normal from two vectors on the surface. (cross product of v1,v2)
function getSurfaceNorm(x1,y1,z1,x2,y2,z2){
// normalise vectors
var d1= Math.hypot(x1,y1,z1);
x1 /= d1;
y1 /= d1;
z1 /= d1;
var d2= Math.hypot(x2,y2,z2);
x2 /= d2;
y2 /= d2;
z2 /= d2;
var norm = {}
norm.x = y1 * z2 - z1 * y2;
norm.y = z1 * x2 - x1 * z2;
norm.z = x1 * y2 - y1 * x2;
return norm;
}
// This defines a block with p1-p2 the height of the corners
// starting top left and clockwise around to p4 bottom left
// If the block is split with 2 slopes then it will be
// of type.split1 or type.split2. If a single slope then it is a type.quad
// Also calculates the normals
function createBlock(x,y,z,h1,h2,h3,h4,type){
var norm1,norm2;
if(type === types.quad){
norm1 = norm2 = getSurfaceNorm(1, 0, h2 - h1, 0, 1, h4 - h1);
}else if(type === types.split1){
norm1 = getSurfaceNorm(1, 0, h2 - h1, 1, 1, h3 - h1);
norm2 = getSurfaceNorm(0, 1, h2 - h1, 1, 1, h3 - h1);
}else{
norm1 = getSurfaceNorm(0, 1, h2-h3, 1, 0, h4 - h3);
norm2 = getSurfaceNorm(1, 0, h2 - h1, 0, 1, h4 - h1);
}
return {
p1 : h1, // top left
p2 : h2, // top right
p3 : h3, // bottom right
p4 : h4, // bottom left
x,y,z,type,
norm1,norm2,
}
}
// get the height on the block at x,y
// also sets the surface block.pNorm to match the correct normal
function getHeight(block,x,y){
var b = block; // alias to make codes easier to read.
if(b.type === types.quad){
b.pNorm = b.norm1;
if(b.p1 === b.p2){
return (b.p3 - b.p1) * (y % 1) + b.p1 + b.z;
}
return (b.p2 - b.p1) * (x % 1) + b.p1 + b.z;
}else if(b.type === types.split1){
if(x % 1 > y % 1){ // on top right side
b.pNorm = b.norm1;
if(b.p1 === b.p2){
if(b.p1 === b.p3){
return b.p1 + b.z;
}
return (b.p3 - b.p1) * (y % 1) + b.p1 + b.z;
}
if(b.p2 === b.p3){
return (b.p2 - b.p1) * (x % 1) + b.p1 + b.z;
}
return (b.p3 - b.p2) * (y % 1) + (b.p2 - b.p1) * (x % 1) + b.p1 + b.z;
}
// on bottom left size
b.pNorm = b.norm2;
if(b.p3 === b.p4){
if(b.p1 === b.p3){
return b.p1 + b.z;
}
return (b.p3 - b.p1) * (y % 1) + b.p1 + b.z;
}
if(b.p1 === b.p4){
return (b.p3 - b.p1) * (x % 1) + b.p1 + b.z;
}
var h = (b.p4 - b.p1) * (y % 1);
var h1 = b.p3 - (b.p4 - b.p1) + h;
return (h1 - (h + b.p1)) * (x % 1) + (h + b.p1) + b.z;
}
if(1 - (x % 1) < y % 1){ // on bottom right side
b.pNorm = b.norm1;
if(b.p3 === b.p4){
if(b.p3 === b.p2){
return b.p2 + b.z;
}
return (b.p3 - b.p2) * (y % 1) + b.p4 + b.z;
}
if(b.p2 === b.p3){
return (b.p4 - b.p2) * (x % 1) + b.p2 + b.z;
}
var h = (b.p3 - b.p2) * (y % 1);
var h1 = b.p4 - (b.p3 - b.p2) + h;
return (h + b.p2 - h1) * (x % 1) + h1 + b.z;
}
// on top left size
b.pNorm = b.norm2;
if(b.p1 === b.p2){
if(b.p1 === b.p4){
return b.p1 + b.z;
}
return (b.p4 - b.p1) * (y % 1) + b.p1 + b.z;
}
if(b.p1 === b.p4){
return (b.p2 - b.p1) * (x % 1) + b.p1 + b.z;
}
var h = (b.p4 - b.p1) * (y % 1);
var h1 = b.p2 + h;
return (h1 - (h + b.p1)) * (x % 1) + (h + b.p1) + b.z;
}
const projection = {
width : 20,
depth : 20, // y axis
height : 8, // z axis
xSlope : 0.5,
ySlope : 0.5,
originX : canvas.width / 2,
originY : canvas.height / 4,
toScreen(x,y,z,point = [],pos = 0){
point[pos] = x * this.width - y * this.depth + this.originX;
point[pos + 1] = x * this.width * this.xSlope + y * this.depth * this.ySlope -z * this.height + this.originY;
return point;
}
}
// working arrays to avoid excessive GC hits
var pointArray = [0,0]
var workArray = [0,0,0,0,0,0,0,0,0,0,0,0,0,0];
function drawBlock(block,col,lWidth,edge){
var b = block;
ctx.strokeStyle = col;
ctx.lineWidth = lWidth;
ctx.beginPath();
projection.toScreen(b.x, b.y, b.z + b.p1, workArray, 0);
projection.toScreen(b.x + 1, b.y, b.z + b.p2, workArray, 2);
projection.toScreen(b.x + 1, b.y + 1, b.z + b.p3, workArray, 4);
projection.toScreen(b.x, b.y + 1, b.z + b.p4, workArray, 6);
if(b.type === types.quad){
ctx.moveTo(workArray[0],workArray[1]);
ctx.lineTo(workArray[2],workArray[3]);
ctx.lineTo(workArray[4],workArray[5]);
ctx.lineTo(workArray[6],workArray[7]);
ctx.closePath();
}else if(b.type === types.split1){
ctx.moveTo(workArray[0],workArray[1]);
ctx.lineTo(workArray[2],workArray[3]);
ctx.lineTo(workArray[4],workArray[5]);
ctx.closePath();
ctx.moveTo(workArray[0],workArray[1]);
ctx.lineTo(workArray[4],workArray[5]);
ctx.lineTo(workArray[6],workArray[7]);
ctx.closePath();
}else if(b.type === types.split2){
ctx.moveTo(workArray[0],workArray[1]);
ctx.lineTo(workArray[2],workArray[3]);
ctx.lineTo(workArray[6],workArray[7]);
ctx.closePath();
ctx.moveTo(workArray[2],workArray[3]);
ctx.lineTo(workArray[4],workArray[5]);
ctx.lineTo(workArray[6],workArray[7]);
ctx.closePath();
}
if(edge){
projection.toScreen(b.x + 1, b.y, b.z, workArray, 8);
projection.toScreen(b.x + 1, b.y + 1, b.z, workArray, 10);
projection.toScreen(b.x, b.y + 1, b.z, workArray, 12);
if(edge === 1){ // right edge
ctx.moveTo(workArray[2],workArray[3]);
ctx.lineTo(workArray[8],workArray[9]);
ctx.lineTo(workArray[10],workArray[11]);
ctx.lineTo(workArray[4],workArray[5]);
}
if(edge === 2){ // right edge
ctx.moveTo(workArray[4],workArray[5]);
ctx.lineTo(workArray[10],workArray[11]);
ctx.lineTo(workArray[12],workArray[13]);
ctx.lineTo(workArray[6],workArray[7]);
}
if(edge === 3){ // right edge
ctx.moveTo(workArray[2],workArray[3]);
ctx.lineTo(workArray[8],workArray[9]);
ctx.lineTo(workArray[10],workArray[11]);
ctx.lineTo(workArray[12],workArray[13]);
ctx.lineTo(workArray[6],workArray[7]);
ctx.moveTo(workArray[10],workArray[11]);
ctx.lineTo(workArray[4],workArray[5]);
}
}
ctx.stroke();
}
function createMap(){
var base = "0".charCodeAt(0);
for(var y = 0; y < mapSize.depth; y ++){
for(var x = 0; x < mapSize.width; x ++){
var index = y * (mapSize.width + 1) + x;
var b;
var p1= map.charCodeAt(index)-base;
var p2= map.charCodeAt(index+1)-base;
var p3= map.charCodeAt(index+1+mapSize.width + 1)-base;
var p4= map.charCodeAt(index+mapSize.width + 1)-base;
var type;
if((p1 === p2 && p3 === p4) || (p1 === p4 && p2 === p3)){
type = types.quad;
}else if(p1 === p3){
type = types.split1;
}else if(p4 === p2){
type = types.split2;
}else{
// throw new RangeError("Map has badly formed block")
type = types.split2;
}
blocks.push(
b = createBlock(
x,y,0,p1,p2,p3,p4,type
)
);
}
}
}
function drawMap(){
for(var i = 0; i < blocks.length; i ++){
var edge = 0;
if(i % mapSize.width === mapSize.width- 1){
edge = 1;
}
if(Math.floor(i / mapSize.width) === mapSize.width- 1){
edge |= 2;
}
drawBlock(blocks[i],"black",1,edge);
}
}
function drawBallShadow(ball){
var i;
var x,y,ix,iy;
ctx.globalAlpha = 0.5;
ctx.fillStyle = "black";
ctx.beginPath();
var first = 0;
for(var i = 0; i < 1; i += 1/8){
var ang = i * Math.PI * 2;
x = ball.x + (ball.rad / projection.width ) * Math.cos(ang) * 0.7;
y = ball.y + (ball.rad / projection.depth ) * Math.sin(ang) * 0.7;
if(x < mapSize.width && x >= 0 && y < mapSize.depth && y > 0){
ix = Math.floor(x + mapSize.width) % mapSize.width;
iy = Math.floor(y + mapSize.depth) % mapSize.depth;
var block = blocks[ix + iy * mapSize.width];
var z = getHeight(block,x,y);
projection.toScreen(x,y,z, pointArray);
if(first === 0){
first = 1;
ctx.moveTo(pointArray[0],pointArray[1]);
}else{
ctx.lineTo(pointArray[0],pointArray[1]);
}
}
}
ctx.fill();
ctx.globalAlpha = 1;
}
function drawBall(ball){
projection.toScreen(ball.x, ball.y, ball.z, pointArray);
ctx.fillStyle = ball.col;
ctx.strokeStyle = "black";
ctx.lineWidth = 2;
ctx.beginPath();
ctx.arc(pointArray[0],pointArray[1],ball.rad,0,Math.PI * 2);
ctx.stroke();
ctx.fill();
ctx.fillStyle = "white";
ctx.beginPath();
ctx.arc(pointArray[0]-ball.rad/2,pointArray[1]-ball.rad/2,ball.rad/4,0,Math.PI * 2);
ctx.fill();
}
function updateBall(ball){
// reset ball if out of bounds;
if(ball.x > mapSize.width || ball.y > mapSize.depth || ball.x < 0 || ball.y < 0){
ball.x += ball.dx;
ball.y += ball.dy;
ball.z += ball.dz;
ball.dz -= 0.1;
if(ball.z < -10){
ball.x = Math.random() * 3;
ball.y = Math.random() * 3;
ball.dz = 0;
// give random speed
ball.dx = Math.random() * 0.01;
ball.dy = Math.random() * 0.01;
}else{
ball.dist = Math.hypot(ball.x - 20,ball.y - 20, ball.z - 20);
return;
}
}
// get the block under the ball
var block = blocks[Math.floor(ball.x) + Math.floor(ball.y) * mapSize.width];
const lastZ = ball.z;
// get the height of the black at the balls position
ball.z = getHeight(block,ball.x,ball.y);
// use the face normal to add velocity in the direction of the normal
ball.dx += block.pNorm.x * 0.01;
ball.dy += block.pNorm.y * 0.01;
// move the ball up by the amount of its radius
ball.z += ball.rad / projection.height;
ball.dz =lastZ - ball.z;
// draw the shadow and ball
ball.x += ball.dx;
ball.y += ball.dy;
// get distance from camera;
ball.dist = Math.hypot(ball.x - 20,ball.y - 20, ball.z - 20);
}
function renderBall(ball){
drawBallShadow(ball);
drawBall(ball);
}
function copyCanvas(canvas){
var can = document.createElement("canvas");
can.width = canvas.width;
can.height = canvas.height;
can.ctx = can.getContext("2d");
can.ctx.drawImage(canvas,0,0);
return can;
}
var map = `
9988888789999
9887787678999
9877676567899
9876765678789
9876655567789
8766555456789
7655554456678
6654443456789
6543334566678
5432345677889
4321234567789
4321234567899
5412345678999
`.replace(/\n| |\t/g,"");
var mapSize = {width : 12, depth : 12}; // one less than map width and depth
var blocks = [];
ctx.clearRect(0,0,canvas.width,canvas.height)
createMap();
drawMap();
var background = copyCanvas(canvas);
var w = canvas.width;
var h = canvas.height;
var cw = w / 2; // center
var ch = h / 2;
var globalTime; // global to this
var balls = [{
x : -10,
y : 0,
z : 100,
dx : 0,
dy : 0,
dz : 0,
col : "red",
rad : 10,
},{
x : -10,
y : 0,
z : 100,
dx : 0,
dy : 0,
dz : 0,
col : "Green",
rad : 10,
},{
x : -10,
y : 0,
z : 100,
dx : 0,
dy : 0,
dz : 0,
col : "Blue",
rad : 10,
},{
x : -10,
y : 0,
z : 100,
dx : 0,
dy : 0,
dz : 0,
col : "yellow",
rad : 10,
},{
x : -10,
y : 0,
z : 100,
dx : 0,
dy : 0,
dz : 0,
col : "cyan",
rad : 10,
},{
x : -10,
y : 0,
z : 100,
dx : 0,
dy : 0,
dz : 0,
col : "black",
rad : 10,
},{
x : -10,
y : 0,
z : 100,
dx : 0,
dy : 0,
dz : 0,
col : "white",
rad : 10,
},{
x : -10,
y : 0,
z : 100,
dx : 0,
dy : 0,
dz : 0,
col : "orange",
rad : 10,
}
];
// main update function
function update(timer){
globalTime = timer;
ctx.setTransform(1,0,0,1,0,0); // reset transform
ctx.globalAlpha = 1; // reset alpha
ctx.clearRect(0,0,w,h);
ctx.drawImage(background,0,0);
// get the block under the ball
for(var i = 0; i < balls.length; i ++){
updateBall(balls[i]);
}
balls.sort((a,b)=>b.dist - a.dist);
for(var i = 0; i < balls.length; i ++){
renderBall(balls[i]);
}
requestAnimationFrame(update);
}
requestAnimationFrame(update);
答案 1 :(得分:1)
要处理带有块的点x的碰撞检测(类似于立方体但允许倾斜的墙壁),从定义块的3个点a,b,c中减去点x,然后计算向量点向量x的乘积与块点向量a&#39;,b&#39;,c&#39;给你3个数字A,B,C。如果这些数字分别在零和矢量a,b,c的长度之间,则矢量x在块内。或者换句话说,每个A /(长度a&#39;平方),B /(长度b&#39;平方),C /(长度c&#39;平方)小于或等于1且大于零。
同样适用于四面体,但附加条件是A /(长度a&#39;平方)+ B /(长度b&#39;平方)+ C /(长度c&#39;平方)较小或等于1。
我希望在这方面没有错,这就是我如何处理这个问题。这也没有处理边缘边缘的两个四面体的碰撞。所以我在这里说的还有改进的余地。特别是,对立方体或四面体的边缘进行碰撞检查会很好。
对于其他形状,将这些形状分成立方体和四面体。
答案 2 :(得分:1)
我认为对于这个游戏来说,计算游乐场/基地的表面就足够了。然后,对于曲面上的每个点,您可以计算所有方向的导数。由于它们都是平面,因此可以简单地表示为常量矩阵。
玩家始终在f( x, y ) = height
,并且∇f( int(x), int(z) ) + v
更改为v
是玩家输入的力量向量。
变化为∞的边界(实心墙)必须单独处理,例如。计算&#39;跳跃&#39;在f(x,y)。
更新1
所以基本上你可以做的是创建以下功能:
function height(x,y)
- &gt;返回一个整数
function slope(x,y)
- &gt;返回两个整数的元组(包括无穷大)
&#39>玩家&#39;从位置(x_0, y_0)
开始,因此身高z_0 = height(x_0, y_0)
。
接下来的位置将是
(x_1, y_1) = (x_0, y_0) + s_g * slope(x_0, y_0) + s_u * v
,身高z_1 = height(x_1, y_1)
v
:表示用户想要应用于“玩家”的移动方向的矢量&#39;。
s_g
:速度/〜重力,
s_u
:加快用户控制播放器的速度。
为了正确处理重力,slope
函数的方向必须反转为正方向。
避免玩家上升90度。连续下坡:
只需将斜率函数的+infinity
值设置为0。