我正在创建 atari 8bit 的重新制作,称为Spindizzy作为我的学校项目。
我想出了如何使用3d points
将其渲染到2d perspective point
。
i也可以旋转形状(呈现效率的较小问题)。
我的目标是这样:
Vector3D -> Vector2D
-完成
Block -> [[Vector3D]] -> [[Vector2D]] -> [Shape]
-完成
Map -> [Location] -> [[[Block]]]
,但我不知道应该什么Chandle Colision检测以及如何将其chandle。
我的目标语言是 js
,但是如果给出的答案是其他语言,或者只是描述了如何解决此问题。
不允许框架。
我正在使用2d
画布的上下文。
在这里您可以看到我的代码。
我会感谢链接和建议。
表面和正常。
忽略投影并仅在3D空间中使用一些假设工作。
- 所有块都与x,y轴对齐。
- 所有块都有每个角的高度。
- 所有块都可以具有一个平坦的表面,也可以分为两个三角形。
- 拆分块要么从左上到右下或从左下角向左下方分开。
- Quad(Unsplit Block)4高点必须在同一平面上。
- 块始终是1个单位宽度(x轴)和1个单位深度(y轴)。
- 可以拥有一个无效的块。要成为有效的块至少2分,必须具有相同的高度。所有高度设置为不同值的块不是有效的块。(这与视频中游戏的条件匹配)
每个块由一个对象定义的一个对象(x,y,z),角度高度,类型(Split1,Split2或Quad)和表面norm/s
单个功能将返回块上x,y处的点的高度。块PNorm属性将在此点设置为正常表面。(请勿修改正常。如果您需要修改它首先创建副本)
表面正常是垂直于平面的线。进行高度测试时,属性block.pNorm
设置为适当的正常。常态用于确定球应滚动的方向。(我没有包括任何Z运动,球被粘在表面上)。正常也用于确定阴影以及球会弹跳的方向。
演示
最好的解释方法是通过演示。有很多代码可以使演示发生,所以请询问您是否有任何疑问。
note 代码用一点ES6编写,因此需要Babel在Legacy浏览器上运行。
update 第一篇文章我有一个我没有发现的错误(错误设置了正态)。我现在已经修复了。我还添加了一个错误,如果地图包含一个无效的块,它将抛出连击。
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);
处理用块的点x碰撞检测(这就像一个立方体,但允许倾斜的墙壁),从3中减去点x定义块的点A,B,C点,然后计算点矢量X的向量乘积与块点向量A',B',C',以给您3个数字A,B,c。如果这些数字分别介于零和向量A,B,C平方的长度之间,则向量X在块内。或另一种方法(每种a/'平方),b/(长度b'平方),c/(长度c'平方)较小或等于1,大于零。
同样适用于四面体,但额外的条件是a/(长度a'squared) b/(长度b'平方) c/(长度C'平方)较小或等于1。
我希望在这方面没有任何错误,这就是我如何解决问题的方式。这也无法处理两个四面体边缘的碰撞。因此,我在这里所说的有改进的余地。特别是,与立方体或四面体的边缘检查边缘会很好。
对于其他形状,将这些形状划分为立方体和四面体。
常规
我认为对于这个游戏,足以计算游乐场/底座的表面。然后,对于表面上的每个点,您可以在各个方向上计算衍生物。由于它们都是平面,因此可以简单地表示为常数矩阵。
播放器始终在f( x, y ) = height
上,并且更改∇f( int(x), int(z) ) + v
,v
是播放器输入的部队向量。
更改为∞(实心壁)的边界必须通过EG分别处理。计算f(x,y)中的"跳跃"。
更新1
方法
基本上您可以做的就是创建以下功能:
-
function height(x,y)
->返回整数- 给出位置(x,y)的基数高度
-
function slope(x,y)
->返回两个整数的元组(包括无穷大)- 在x方向和y方向上放置基数的斜率(x,y)
"播放器"从位置(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
:代表用户想要应用于"播放器"的运动方向的向量。
s_g
:速度/〜重力,
s_u
:加快用户控制玩家。
要正确处理重力,必须将slope
功能的方向逆转为正方向。
避免播放器上升90度。连续下坡:只需将坡度函数的+infinity
值设置为0。