1
\$\begingroup\$

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:

enter image description here

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:

enter image description here

Any ideas how to fix this -- without using any kind of external libraries etc. (only what's available in the snippet)?

\$\endgroup\$
18
  • 1
    \$\begingroup\$ Being an effective coder consists of more than just taking other people's code and putting it into your codebase. You need to have broken down and studied the details of what Christoph gave you in 2018, and been able to build it up yourself again, step by step, with each step working in sequence. Keep getting more answers on the internet, and you'll get more lost and unable to work with your own code. Instead, you should be breaking down the code and asking the question for each function you don't grok, "What does this do? How does this work?" In that way you may understand how to fix it. \$\endgroup\$
    – Engineer
    May 3, 2020 at 8:22
  • 1
    \$\begingroup\$ OK. Do you understand the general principle behind why you are having problems at the outer corners and not the inner ones? This is key for you to be able to write your own code this time, to solve the problem. So I'm going to be annoying and ask you questions, like a teacher would. The last thing you want is to still not understand the problem after this. If I just give you code, that may happen again. \$\endgroup\$
    – Engineer
    May 4, 2020 at 21:19
  • 1
    \$\begingroup\$ You can also prove what I'm saying by making two adjacent walls run parallel (no angle between them). The moment they are parallel, you will either have the same problem on both sides where they join, or no problem on either side (again, depends on how that code is written). \$\endgroup\$
    – Engineer
    May 4, 2020 at 22:35
  • 1
    \$\begingroup\$ Let us continue this discussion in chat. \$\endgroup\$ May 4, 2020 at 22:40
  • 1
    \$\begingroup\$ If you understand it all, then you should be good to solve it yourself. Have you sat down with a pen and paper like I suggested? Have you started writing a minimal test case to probe the problem for solutions? \$\endgroup\$
    – Engineer
    May 5, 2020 at 15:38

2 Answers 2

1
\$\begingroup\$

You're not handling line endpoints correctly. Consider only a single line segment. The collision of a circle at the endpoint of a single line segment should be rounded, right? Like, if you trace out where the center of the circle should be stopped as you hit the endpoint from various angles, that will form a circle around the endpoint. Let's call this the player center's collision area. Now, you are correctly determining when the player's center has entered that area, but when it happens, you're using the same code to push the player away as you use when they hit the side of the line, which moves them to a point aligned with the line's endpoint, pushed out perpendicular to the line segment by the distance of their radius. This effectively pops them to the side of the line - to the same place regardless of where they touched the player center's collision area. If they've been sliding along the line segment and are trying to slide off the endpoint, this effectively pulls them back so they can't move forward (until they rotate to face totally away from the line). That's why you get stuck.

The solution is to change your collision resolution at the endpoints to push the player in the right direction: away from the endpoint. This is easy because you're already calculating the nearest point on the line and clamping it to the endpoints. So, instead of using the line's delta to calculate a normal vector to push them away, just take the direction from the closest point on the line ("projection" in your code) to the player's center ((x1,y1) - projection), normalize it and multiply that by the radius. This will always push them away, leaving them at the edge of the player's center collision area near where they entered it, so it won't pull them back as they try to round a corner. This will even work properly from both sides of the line segment.

\$\endgroup\$
9
  • \$\begingroup\$ even if it collides with mutliple lines at once, thus colliding with 2 separate corners [which are really the same point], this will still work? \$\endgroup\$ Jul 10, 2022 at 7:34
  • 1
    \$\begingroup\$ It should, because both line endpoints are the same point, so pushing the player away from them should resolve to the same location for both lines. \$\endgroup\$ Jul 10, 2022 at 7:36
  • \$\begingroup\$ Hi thanks I implemented what you suggested, but see the new edit, it looks like it kind of works, but since the player is still moving the same direction it was before it keeps re-hitting the wall \$\endgroup\$ Jul 10, 2022 at 8:21
  • \$\begingroup\$ Also based on tests I've just run it appears that for some reason it makes a difference, pushing away from the point, depending on which direction it is colliding by [which is probably because that's the first collision resolved by the for loop], that it looks like it pushes it away from the direction of the line which the point is a part of, as u can see if u approach the line from different sides. Not sure why this is considering it's just one point \$\endgroup\$ Jul 10, 2022 at 8:34
  • 1
    \$\begingroup\$ Ohh, I think this is an issue with the order of the line segment checks. The player is moving into the wall a bit, gets pushed by the endpoint of the upper wall, which pushes it down a bit, then gets pushed out of the lower wall normally, but that little push down is making it hard to travel upward. That's going to be harder to fix. If you could always check the closer line segment first, that would probably fix it, but it's a big change. You could split your movement up into multiple smaller movements, and that would alleviate the issue somewhat. I'll think about it... \$\endgroup\$ Jul 10, 2022 at 9:12
0
\$\begingroup\$

I don't know much about javascript, but it seems like you are moving from pt1 to pt2 when you are colliding a wall. Notice that it depends what direction you are going. If you go on a wall and slide and block to the corner, when you go on the other wall sharing the same corner, you can slide and it will exit the wall, where on the other wall it was blocking. Now, if I get this right, you are sliding on the wall using pt1 to pt2, and it seems like depending on which corner is pt1 and which is pt2, you won't block on one of them. That means the order of the wall points changes something and somewhere you are saying that pt1 won't block and pt2 will block, or vice versa. If you understand any of this script, you should be able to fix this. Else, like the others said, you should try to understand it first, play around with it, than try to fix the problem.

\$\endgroup\$
1
  • \$\begingroup\$ The reason why it works for some and not others is because the order it loops through the line segments. But even for the side that "works", it's only if it's slid off from a very specific angle, but heading at it straight doesn't work. Tried taking out collision for corners altogether, nope. Tried continuing to slide based on previous motion, nope (jitters at very end, not smooth but technically is more than nothing). Tried pushing it back along a line perpendicular to the corner, but since the movement is still in the same direction, continuously towards it, created just more jittering \$\endgroup\$ Jul 5, 2022 at 15:00

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .