this is a follow up to this other question: How do I handle player collision with corners of a wall
In inspiration of the code given in its answer, I tried to write some new code.
Basically, in the original, the wall sliding works very well on the inside of the walls, but I wanted to make it work on the outside as well, so I made a new basic code engine, based on his technique:
BH
<br>
<meta charset="utf-8">
<button onclick="start()" class=wow>ok</button>
<canvas id=can>
</canvas>
<style>
canvas{
width:100%;
height:100%;
position:absolute;
top:0;
left:0px
}
.wow{
float:right;
z-index:1298737198
}
</style>
<script>
var aD =[]
var r
function start() {
r = new CanvasRenderer(can),
my = new scene();
window.my = my
eventHandler();
my.add(new mesh({
name:"ora",
verts: [
0, 0, //0,
100, 15, //1,
175, 40,
50, 100,
20, 75,
2,25
],
position: {
x: 100,
y:100,
z:1
},
scale: {
x:1,y:1,z:1
},
color:"orange",
onupdate(me) {
if(!me.dx) {
me.dx=0//.3
}
me.position.x+=me.dx
me.position.y+=me.dx/3
me.rotation.x+=me.dx*8*Math.PI/180
if(me.position.x>=500) {
me.position.x--;
me.dx = -0.3
}
if(me.position.x<=0) {
me.position.x++
me.dx = 0.3
}
}
}));
my.add(new mesh({
name:"tri",
verts: [
0, 0, //0,
80, 35, //1,
30, 70,
28, 30,
5, 80,
4, 30
],
position: {
x: 100,
y:200,
z:1
},
rotation:{
x:-20*Math.PI/180
},
scale: {
x:2,y:2,z:1
},
color:"yellow",
onupdate(me) {
if(!me.dx) {
me.dx=0//-0.2
}
me.position.x+=me.dx
me.position.y+=me.dx/3
me.rotation.x+=me.dx*8*Math.PI/180
if(me.position.x>=500) {
me.position.x--;
me.dx = -0.3
}
if(me.position.x<=0) {
me.position.x++
me.dx = 0.3
}
}
}));
var g = false
my.add(new mesh({
primitive:"rect",
name: "player",
scale: {
x: 50,
y:50
},
position: {
x: 311,
y:75,
z:13
},
origin: {
x:0.5,
y:0.5,
z:0.5
},
onupdate(me) {
var upKey = keys[38],
downKey = keys[40],
rightKey = keys[39],
leftKey = keys[37],
drx = 0,
dx = 0,
speed = 5,
turningSpeed = 3
drx = leftKey ? -1 : rightKey ? 1 : 0
forward = upKey ? 1 : downKey ? -1 : 0
if(!window.aa)aa=me
me.rotation.x += (
(drx * Math.PI / 180 * turningSpeed )
)
me.rotation.y = 1;
var xDir = Math.cos(me.rotation.x)
var yDir = Math.sin(me.rotation.x)
me.position.x += xDir * forward * speed
me.position.y += yDir * forward * speed
var lines = []
for(var i = 0; i < my.objects.length; i++) {
let cur = my.objects[i];
if(cur.name !== me.name) {
cur.lineSegments.forEach(l => {
//if(lines.length>=2) return
var col = checkCollision(
me.position.x,
me.position.y,
me.position.z,
me.scale.x/2,
l
)
if(col) {
if(!me.curLine&&!col.iscorner)
me.curLine = l
// else
lines.push(l)
}
});
}
}
if(me.curLine) {
var oldL = lines.length
lines=insertAtArray(0,me.curLine,lines)
//remove duplicates
lines = uniq(lines)
/*
old code remove curr
lines.filter((v,ind,ar)=>
ar.indexOf(v)==ind
);
var sl = lines.slice(1)
var indo = sl.indexOf(me.curLine)
if(indo > -1)
lines.splice(indo,1)*/
//if(lines.length>=3) lines.splice(lines.length-1,1)
}
var fc = false
lines.forEach(a=>{
var col = checkCollision(
me.position.x,
me.position.y,
me.position.z,
me.scale.x/2,
a
)
if(col) {
me.position.y = col.y
me.position.x = col.x
if(a==me.curLine)
fc=true
}
})
me.lines=lines
//\
if(!fc) me.curLine=null;
}
}));
let i = setInterval(() => render(r, my), 16);
r.on("resize", () => render(r, my));
}
function uniq(arr) {
return arr.map(JSON.stringify)
.filter(
(el , i , arr)=>
i === arr.indexOf(el)
).map(JSON.parse)
}
function insertAtArray(at,val,array) {
return array = array.slice(0,at).concat(val)
.concat(array.slice(at))
}
function checkCollision(x1, y1, z1, rad,l) {
var dist = distance2(
l.start[0],
l.start[1],
l.end[0],
l.end[1]
),
vec1 = [
x1 - l.start[0],
y1 - l.start[1],
z1 - l.start[2]
],
vec2 = [
l.end[0] - l.start[0],
l.end[1] - l.start[1]
],
percentOfWall = (
Math.max(
0,
Math.min(
1,
dot(
vec1[0],
vec1[1],
vec2[0],
vec2[1]
) / dist
)
)
),
projection = [
l.start[0] + percentOfWall * vec2[0],
l.start[1] + percentOfWall * vec2[1],
],
acDist = Math.sqrt(distance2(
x1, y1,
projection[0], projection[1]
))
aD.push( () => {
r.ctx.beginPath()
r.ctx.fillStyle="green"
r.ctx.arc(projection[0], projection[1], 5, 0, Math.PI*2);
r.ctx.fill()
r.ctx.closePath();
})
if(acDist < rad) {
var mag = Math.sqrt(dist),
delt = [
l.end[0] - l.start[0],
l.end[1] - l.start[1]
],
normal = [
delt[0] / mag,
delt[1] / mag
]
var x;
var y;
var iscorner=false
if(
percentOfWall == 0 ||
percentOfWall == 1
//case of corner[s]
) {
var distFromProjToPlCenter = [
x1 - projection[0],
y1 - projection[1]
];
var distNormalized = normalize(
distFromProjToPlCenter
)
var scaled = scaleVec(
distNormalized, rad
)
x = projection[0]+scaled[0]
y = projection[1]+scaled[1]
iscorner=true
} else {
x = projection[0] +
rad * (normal[1] )
y = projection[1] +
rad* (-normal[0] )
}
return {
x,
y,
projection,
normal,
iscorner
}
}
}
function scaleVec(vec,sc) {
return [
vec[0]*sc,
vec[1]*sc
]
}
function dot(x1, y1, x2, y2) {
return (
x1 * x2 + y1 * y2
)
}
function normalize(p) {
var x = p[0]
var y = p[1]
//(x/(x^2 + y^2)^(1/2), y/(x^2 + y^2)^(1/2))
var val = Math.sqrt(
x * x +
y * y
)
return [
x / val,
y / val
]
}
function distance2(x1, y1, x2, y2) {
let dx = (x1 - x2), dy = (y1 - y2);
return (
dx * dx + dy * dy
);
}
function render(r,s) {
//r.ctx.clearRect(0,0,r.ctx.canvas.width,r.ctx.canvas.height)
s.update();
r.render(s)
aD.forEach(x=>x());
aD = []
}
onload = start;
function eventHandler() {
window.keys = {};
addEventListener("keyup" , e=> {
keys[e.keyCode] = false;
});
addEventListener("keydown" , e=> {
keys[e.keyCode] = true;
});
}
function CanvasRenderer(dom) {
if(!dom) dom = document.createElement("canvas");
var events = {}, self = this;
function rsz() {
dom.width = dom.clientWidth;
dom.height = dom.clientHeight;
self.dispatchEvent("resize");
}
window.addEventListener("resize", rsz);
let ctx = dom.getContext("2d");
function render(scene) {
ctx.clearRect(0,0,ctx.canvas.width,ctx.canvas.height);
for(let i = 0; i < scene.objects.length; i++) {
let o = scene.objects[i],
verts = o.realVerts;
ctx.beginPath();
ctx.moveTo(
verts[0] ,
verts[1]
);
verts.forEach((v, i, ar) => {
let y = i;
ctx.lineTo(
v[0] ,
v[1]
);
});
ctx.lineTo(
verts[0],
verts[1]
);
ctx.fillStyle = o.color || "blue";
ctx.lineWidth = 1;
ctx.fill()
ctx.stroke();
ctx.closePath();
}
}
Object.defineProperties(this, {
domElement: {
get: () => dom
},
ctx: {
get: () => ctx
},
render: {
get: () => render
},
on: {
get: () => (nm, cb) => {
if(!events[nm]) {
events[nm] = [];
}
events[nm].push(data => {
if(typeof cb == "function") {
cb(data);
}
});
}
},
dispatchEvent: {
get: () => (name, data) => {
if(events[name]) {
events[name].forEach(x => {
x(data);
});
}
}
}
});
rsz();
}
function scene() {
let objects = [];
Object.defineProperties(this, {
add: {
get: () => obj => {
objects.push(obj);
}
},
objects: {
get: () => objects
},
update: {
get: () => () => {
objects.forEach(x => {
if(typeof x.update == "function") {
x.update();
}
});
}
}
});
}
function mesh(data={}) {
let verts = [],
self = this,
holder = {
position:{},
scale: {
},
rotation: {},
origin:{}
},
actual = {
},
position = {},
scale = {},
rotation = {},
origin = {},
color,
name,
primitive,
eventNames = "update",
events = {},
drawPrimitive = {
circle(ctx) {
ctx.beginPath();
ctx.arc(
self.position.x,
self.position.y,
5,
0,
360 * Math.PI / 180
);
ctx.closePath();
},
rect(ctx) {
ctx.strokeRect(
self.position.x,
self.position.y,
30, 30
);
}
},
width = 1,
height = 1,
primitiveToVerts = {
rect: () => [
0, 0,
width , 0,
width, height,
0, height
]
},
realVerts = verts,
lineSegments = [],
o = this;
function updateRealVerts() {
let actualVerts = [],
originedVerts = [],
adjustedVerts = [],
rotatedVerts = [],
stepSize = o.step || 2,
curVerts = [];
o.verts.forEach((v, i) => {
curVerts.push(v);
if(
(i - 1) % stepSize === 0 &&
i !== 0
) {
actualVerts.push(curVerts);
curVerts = [];
}
});
actualVerts = actualVerts.filter(x => x.length == stepSize);
originedVerts = actualVerts.map(v => [
v[0] - o.origin.x,
v[1] - o.origin.y,
v[2] - o.origin.z
]);
rotatedVerts = originedVerts.map(v =>
[
v[0] * Math.cos(o.rotation.x) -
v[1] * Math.sin(o.rotation.x),
v[0] * Math.sin(o.rotation.x) +
v[1] *Math.cos(o.rotation.x),
v[2]
]
);
adjustedVerts = rotatedVerts.map(v =>
[
v[0] *
o.scale.x +
o.position.x,
v[1] *
o.scale.y +
o.position.y,
v[2] *
o.scale.z +
o.position.z,
]
);
realVerts = adjustedVerts;
updateLineSegments();
}
function updateLineSegments() {
let lines = [];
for(let i = 0, a = realVerts; i < a.length;i++) {
let start = [], end = []
if(i < a.length - 1) {
start = a[i];
end = a[i + 1];
} else {
start = a[i];
end = a[0];
}
lines.push({
start, end,
goof:o.name
})
}
lineSegments = lines;
}
Object.defineProperties(position, {
x: {
get: () => holder.position.x || 0,
set: v => holder.position.x = v
},
y: {
get: () => holder.position.y || 0,
set: v => holder.position.y = v
},
z: {
get: () => holder.position.z || 0,
set: v => holder.position.z = v
}
});
Object.defineProperties(scale, {
x: {
get: () => holder.scale.x || 1,
set: v => holder.scale.x = v
},
y: {
get: () => holder.scale.y || 1,
set: v => holder.scale.y = v
},
z: {
get: () => holder.scale.z || 1,
set: v => holder.scale.z = v
}
});
Object.defineProperties(rotation, {
x: {
get: () => holder.rotation.x || 0,
set: v => holder.rotation.x = v
},
y: {
get: () => holder.rotation.y || 0,
set: v => holder.rotation.y = v
},
z: {
get: () => holder.rotation.z || 0,
set: v => holder.rotation.z = v
}
});
Object.defineProperties(origin, {
x: {
get: () => holder.origin.x || 0,
set: v => holder.origin.x = v
},
y: {
get: () => holder.origin.y || 0,
set: v => holder.origin.y = v
},
z: {
get: () => holder.origin.z || 0,
set: v => holder.origin.z = v
}
});
Object.defineProperties(this, {
verts: {
get: ()=>verts,
set(v) {
verts = v
}
},
name: {
get: ()=>name,
set(v) {
name = v
}
},
primitive: {
get: ()=>primitive,
set(v) {
primitive = v;
let newVerts = primitiveToVerts[v];
if(newVerts) {
this.verts = newVerts();
}
}
},
width: {
get: ()=>width,
set(v) {
width = v
}
},
height: {
get: ()=>height,
set(v) {
height = v
}
},
position: {
get: () => position,
set: v => {
position.x = v.x || 0;
position.y = v.y || 0;
position.z = v.z || 0;
}
},
scale: {
get: () => scale,
set: v => {
scale.x = v.x || v.x === 0 ? v.x : 1;
scale.y = v.y || v.y === 0 ? v.y : 1;
scale.z = v.z || v.z === 0 ? v.z : 1;
}
},
rotation: {
get: () => rotation,
set: v => {
rotation.x = v.x || 0;
rotation.y = v.y || 0;
rotation.z = v.z || 0;
}
},
origin: {
get: () => origin,
set: v => {
origin.x = v.x || 0;
origin.y = v.y || 0;
origin.z = v.z || 0;
}
},
color: {
get: () => color,
set: v => {
color = v;
}
},
realVerts: {
get: () => realVerts
},
lineSegments: {
get: () => lineSegments
},
update: {
get: () => () => {
if(events["update"]) {
events.update.forEach(x => {
updateRealVerts();
x(this);
});
}
}
},
on: {
get: () => (nm, fnc) => {
if(!events[nm]) events[nm] = [];
events[nm].push(stuff => {
if(typeof fnc == "function") {
fnc(stuff);
}
});
}
}
});
eventNames.split(" ").forEach(x => {
var name = "on" + x;
if(!this.hasOwnProperty(name)) {
Object.defineProperty(this, name, {
get: () => events[name],
set(v) {
events[x] = [
data => {
typeof v == "function" && v(data)
}
];
}
});
}
});
for(let k in data) {
this[k] = data[k]
}
updateRealVerts();
}
</script>
See "checkCollision" function and player loop for the relevant parts.
[Edit] It almost works as expected, but [as you can see in the following gif] it gets stuck only when caught between two objects, not sure why:
This version shows that the issue exists in any crevice, even within one object.
And either way if the angle is very small the player goes right through the corner:
Any ideas how to fix this -- without using any kind of external libraries etc. (only what's available in the snippet)?