Handling ceiling slopes & corner correction (I'm so close, looking for help)

Not favoritedFavorited Favorited 0 favourites
  • 5 posts
From the Asset Store
Game with complete Source-Code (Construct 3 / .c3p) + HTML5 Exported.
  • I'm currently in the process of "modding" the platformer behavior (without hacks) to handle slopes on the ceiling in a neat way. I do believe I'm really close, but there's some jank left. So I'm posting my code here in hopes some wizard can do magic and of course in return everyone is free to use this code.

    wackytoaster.at/parachute/ceilingSlopes.c3p

    There's some jank when jumping into a place where two ceilings sort of meet. Just try around in the project you'll surely encounter it. The player then jitters around and it doesn't look too nice.

    I probably ideally also add another function to handle the slopes when falling, because when you rub the wall when falling, the player seems to have their vectorX set to 0 by the behavior because it sees a "wall"

    In any case, I hope someone can help me make this as good as possible.

  • I had a quick look but can’t test since I’m not on a pc.

    Bear in mind that using a ray cast only approximates the collision normal between two overlapping polygons. Thats probably at least partially why it doesn’t work well between two solids. A more elaborate algorithm such as SAT (separating axis theorem) would give a more precise normal between polygons, or you may be able to to do multiple raycasts and maybe average the normals to get a better approximation.

    Another reason you may want to do multiple ray casts is one ray will only hit one of the solids and it won’t necessarily be the one the player is overlapping. Even so, it still seems a bit hit and miss doing it with raycasts vs SAT.

    After the ray cast and comparing the normal you push out of the solids with a loop. First you say you try to push out left then right. The push out right is not doing what you think it’s doing. You’re just moving at the opposite angle which is back into the solids.

    I also see you’re using the reflection angle when doing the pushing. You possibly could try using the normal instead.

  • A more elaborate algorithm such as SAT (separating axis theorem) would give a more precise normal between polygons

    Yeah I read about SAT here and there but implementing it is a different story. The reason why I did it with a raycast was that it's a simple solution that I'm familiar with, even if not perfect. But I might have to go for "perfection" here because of the jank.

    After the ray cast and comparing the normal you push out of the solids with a loop. First you say you try to push out left then right. The push out right is not doing what you think it’s doing. You’re just moving at the opposite angle which is back into the solids.

    Uhm I do believe I have the logic correct here though. I first attempt to push out to the left, if that succeeds I keep the position and don't push out to the right anymore. And if pushing out to the left fails, I reset the position, then attempt to push out to the right. If that succeeds I keep the new position, otherwise I reset again (no pushout). Maybe not the cleanest code though but it's WIP :) I suppose a better version would be to do both attempts and see which direction needs the least pushout to work, then pick that one.

    I also see you’re using the reflection angle when doing the pushing. You possibly could try using the normal instead.

    I did try the normal angle which zooms the player along the slopes. I think I ended up using the reflection angle because it conveniently simulates a kind of friction and slows the player down more if the slop is less steep. Otherwise the player would zoom along the slope at crazy speeds.

    Nvm I was silly and my brain kind of didn't compute that the reflection angle is not what I was actually looking for. I was indeed looking for the normal angle.

    My basis for this was this tutorial for corner correction and I just kinda tacked on from there.

    youtube.com/watch

  • Ok so after checking the code again I did realize I was indeed doing all kinds of nonsense that somehow worked out lol. I revised the code, now using two raycasts and some checks and I got the jittering under control. Actually it works really well overall now, (almost) no jank and framerate independent. Thanks for the input r0j0

    Three issues remain that I think I can fix in due time.

    1. If the player jumps when wedged under a slope, the behavior will stop the player from executing the jump before my code runs. That should be a somewhat easy fix.

    2. The slopes allow the player to combine the behaviors vectorX with whatever vector the pushout creates, causing the player to exceed the usual maximum horizontal speed. Not 100% certain how I can counteract that yet. I'm thinking something like calculating the vectorX of the pushout and subtracting that from the vector of the behavior. Haven't tested that yet though.

    3. Rubbing against a slope stops the player rather than sliding along it. Not sure how yet but it should be reasonably easy to work around I think.

    Code for V2 to just replace all of player.js

    import * as Util from "./util.js";
    
    export class Player extends ISpriteInstance {
    	
    	#preTick = () => this.preTick();
    	#postTick = () => this.postTick();
    	
    	constructor() {
    		super();
    		runtime.addEventListener("pretick", this.#preTick);
    		runtime.addEventListener("tick2", this.#postTick);
    
    		this.storedY = 0;
    		this.didPushout = false;
    	}
    
    	preTick() {
    		// this ticks BEFORE behaviors
    		// TODO: Ideally I should have an input check here to see if a jump was executed because if the player is wedged under a sloped ceiling, the behavior will set vectorY to 0 and bonk the player before the slope handling kicks in
    		this.handleCeilingSlopes();
    	}
    
    	postTick() {
    		// this ticks AFTER behaviors
    		// if a pushout happened this tick, set the players vectorY to the stored vectorY. This is needed because the behavior will set the vectorY to 0 because it detected a ceiling
    		if(this.didPushout) this.behaviors.Platform.vectorY = this.storedY+this.behaviors.Platform.gravity*runtime.dt;
    	}
    
    	handleCeilingSlopes() {
    		this.didPushout = false;
    
    		// Player is not moving upwards, return
    		if (this.behaviors.Platform.vectorY >= 0) return;
    		
    		// Store current position and expected next position based on velocity
    		const old = {"x": this.x, "y": this.y};
    		const next = {"x": this.x+this.behaviors.Platform.vectorX*runtime.dt, "y": this.y+this.behaviors.Platform.vectorY*runtime.dt};
    		let maxPushoutDistance = 7; // maximum corner correction
    		const maxPushoutSlope = 1000*runtime.dt; // max slope correction
    		const minCeilingAngleTolerance = Math.PI*0.05; // maximum ceiling angle
    
    		// set position to next frame
    		this.setPosition(next.x, next.y);
    
    		let overlap = this.testOverlapSolid();
    		if(overlap) {
    			// store the behaviors current vectorY
    			this.storedY = this.behaviors.Platform.vectorY+this.behaviors.Platform.gravity*runtime.dt;
    			let testRight = false;
    			let testLeft = false;
    
    			// test push right
    			let i = 0;
    			let pushoutAngle = 0;
    			while(overlap && i < maxPushoutDistance) {
    				i++;
    				const bbox = this.getBoundingBox();
    				const ray = this.behaviors.LineOfSight.castRay(bbox.left, bbox.top+16, bbox.left, bbox.top-2); // angle defaults to 0 if no slope hit
    				let rAngle = Util.isWithinAngle(ray.normalAngle, Math.PI*0.5, minCeilingAngleTolerance) ? ray.normalAngle-Math.PI*0.5 : ray.normalAngle;
    				if(Math.cos(rAngle) < 0) rAngle = Math.PI*2;
    				if (ray.didCollide && !Util.isWithinAngle(ray.normalAngle, Math.PI*0.5, minCeilingAngleTolerance)) maxPushoutDistance = maxPushoutSlope;
    				this.x += Math.cos(rAngle);
    				this.y += Math.sin(rAngle);
    				overlap = this.testOverlapSolid();
    				pushoutAngle = rAngle;
    			}
    			testRight = {"success": false, "i": i, "x": this.x, "y": this.y, "angle": pushoutAngle};
    			if(!overlap) testRight.success = true;
    
    			// reset
    			this.setPosition(next.x, next.y);
    			overlap = true;
    			maxPushoutDistance = 7;
    			pushoutAngle = 0;
    
    			// test push left
    			i = 0;
    			while(overlap && i < maxPushoutDistance) {
    				i++;
    				const bbox = this.getBoundingBox();
    				const ray = this.behaviors.LineOfSight.castRay(bbox.right, bbox.top+16, bbox.right, bbox.top-2);
    				let rAngle = ray.didCollide ? ray.normalAngle : Math.PI; // angle defaults to Math.PI if no slope hit
    				rAngle = Util.isWithinAngle(rAngle, Math.PI*0.5, minCeilingAngleTolerance) ? rAngle+Math.PI*0.5 : rAngle;
    				if(Math.cos(rAngle) > 0) rAngle = Math.PI;
    				if (ray.didCollide && !Util.isWithinAngle(ray.normalAngle, Math.PI*0.5, minCeilingAngleTolerance)) maxPushoutDistance = maxPushoutSlope;
    				this.x += Math.cos(rAngle);
    				this.y += Math.sin(rAngle);
    				overlap = this.testOverlapSolid();
    				pushoutAngle = rAngle;
    			}
    			testLeft = {"success": false, "i": i, "x": this.x, "y": this.y, "angle": pushoutAngle};
    			if(!overlap) testLeft.success = true;
    
    			// result
    			if(!testLeft.success && !testRight.success) {
    				this.setPosition(old.x, old.y);
    			} else if(testLeft.success && testRight.success) {
    				// detect inner corner
    				if(Math.sign(Math.cos(testLeft.angle)) != Math.sign(Math.cos(testRight.angle))) {
    					this.didPushout = false;
    				} else {
    					this.didPushout = true;
    				}
    				if(testRight.i >= testLeft.i) {
    					this.setPosition(testLeft.x, testLeft.y);
    				} else {
    					this.setPosition(testRight.x, testRight.y);
    				}
    			} else if (testRight.success && !testLeft.success) {
    				this.setPosition(testRight.x, testRight.y);
    				this.didPushout = true;
    			} else if (!testRight.success && testLeft.success) {
    				this.setPosition(testLeft.x, testLeft.y);
    				this.didPushout = true;
    			}
    
    		} else {
    			// no pushout needed, just revert
    			this.setPosition(old.x, old.y);
    		}
    	}
    }
  • Try Construct 3

    Develop games in your browser. Powerful, performant & highly capable.

    Try Now Construct 3 users don't see these ads
  • Alright final update for now. I've updated the project (same link)

    wackytoaster.at/parachute/ceilingSlopes.c3p

    I did add a velocity-based pushout that solves issue #2 mentioned above, but adds some other issues in that it sporadically does not work. So it's disabled in the project. It's kinda fun though.

    Issue #1 cannot be solved as easily as expected, the issue is not that the jump isn't executed by the behavior, but rather that the pushout actually pushes the player into the floor and thus the pushout fails. I tried changing the players height slightly during the pushout to compensate but that also did weird things. I feel like this should work though, maybe I made an error somewhere.

    Issue #3 is a bit more complex than expected, probably another can of worms that I don't wanna open at this moment. I had a solution that kind of worked but was framerate dependent, so... nah.

    For now I'm actually fine with all these 3 issues being present (for my project). It's not make or break for me. But of course, if someone happens to find a solution for them, let me know.

Jump to:
Active Users
There are 1 visitors browsing this topic (0 users and 1 guests)