Post subject: Writing platforming code
Editor
Joined: 3/10/2010
Posts: 899
Location: Sweden
I am trying to make a platforming game. The problem is, platforming physics code is difficult to get right. (As has been proven several times over here) As such, I am looking for someone to team up with me for the game. I already have a great artist and a working engine, I just need help with the platforming code. I am doing this in Flash, so I am looking for someone who doesn't hate the guts of the platform.
Spikestuff
They/Them
Editor, Publisher, Expert player (2299)
Joined: 10/12/2011
Posts: 6339
Location: The land down under.
The best I could hunt down without much attention used: IGNORE ME I LINKED SOMETHING USELESS
WebNations/Sabih wrote:
+fsvgm777 never censoring anything.
Disables Comments and Ratings for the YouTube account. Something better for yourself and also others.
Editor, Skilled player (1505)
Joined: 7/9/2010
Posts: 1317
The video Spikestuff suggested is not very helpful, because it only says copy-paste the code, without saying what's happening. Only skiddies do that. I'm not really good at programming games, but I can help a bit. For collision you can use a if-statement, the character is on ground if right side of the character is greater/equal the left side of a block, then if the left side of the character is lower/equal the right side of a block, then if the bottom of the character is greater/equal the top of a block, then the character stands on a block. You propably can change the order of the if-statements to prevent pointless calculations. For jumping you can either a linear jumping curve, which means jump for a specific time with the same y-speed and then fall down with the same speed. But that doesn't look very nice. Instead you can use a parabolic function to create a smoother jumping curve, the speed starts fast, gets slower to the top and accelerates while falling down. You also need a variable which contains information, if the character is in the air. Drawing the game simplified on a piece of paper and look what conditions need to be true can help.
Favorite animal: STOCK Gt(ROSA)26Sortm1.1(rtTA,EGFP)Nagy Grm7Tg(SMN2)89Ahmb Smn1tm1Msd Tg(SMN2*delta7)4299Ahmb Tg(tetO-SMN2,-luc)#aAhmb/J YouTube Twitch
Active player (306)
Joined: 8/21/2012
Posts: 429
Location: France
I'm not really a programmer, so I'll talk about gameplay ^^. You'll have to make choices about how the character controls. It will influence the code. For example, do you want the jump height to be fixed or variable depending on how long the player holds the button? There is also inertia... Do the character run at max speed as soon as you start moving? If not, how long does it take to reach that speed? Same thing about stopping, and does the character skid when you want to stop from a high speed or change direction sharply? Another related point is how much control you want to have when you are in the air, inertia again. You can compare games like SMB or Sonic (for the "inertia ones") to Megaman (very little to no inertia). Maybe the easier is to start without any inertia for testing, then adding it to fit what you want to have in terms of gameplay, but again, I'm not a coder.
Skilled player (1706)
Joined: 9/17/2009
Posts: 4952
Location: ̶C̶a̶n̶a̶d̶a̶ "Kanatah"
TASeditor wrote:
The video Spikestuff suggested is not very helpful, because it only says copy-paste the code, without saying what's happening. Only skiddies do that.
Well, most of my knowledge on what a section of code does comes from copy pasting an example online, then messing around with it. Not the best way, but it works fine assuming its a hobby and not your job to make the game. :P
Editor, Skilled player (1505)
Joined: 7/9/2010
Posts: 1317
jlun2 wrote:
TASeditor wrote:
The video Spikestuff suggested is not very helpful, because it only says copy-paste the code, without saying what's happening. Only skiddies do that.
Well, most of my knowledge on what a section of code does comes from copy pasting an example online, then messing around with it. Not the best way, but it works fine assuming its a hobby and not your job to make the game. :P
But what if henke37 wants to understand what he is doing. :P
Favorite animal: STOCK Gt(ROSA)26Sortm1.1(rtTA,EGFP)Nagy Grm7Tg(SMN2)89Ahmb Smn1tm1Msd Tg(SMN2*delta7)4299Ahmb Tg(tetO-SMN2,-luc)#aAhmb/J YouTube Twitch
Banned User, Former player
Joined: 3/10/2004
Posts: 7698
Location: Finland
jlun2 wrote:
Well, most of my knowledge on what a section of code does comes from copy pasting an example online, then messing around with it. Not the best way, but it works fine assuming its a hobby and not your job to make the game. :P
It reminds me a bit of the really bad advice that was very popular in the 70's and 80's, and sadly persists to some extent to this day, that "to learn to program you should take other people's code and study them". I like to compare that to saying "to learn how to cook, you should go to restaurants and order meals." You learn cooking best by reading a good book or taking classes, and the same is true for programming. (Although the difference is that programming is more complex and thus requires more books and classes.)
Zarmakuizz
He/Him
Joined: 10/12/2013
Posts: 279
Location: France
Grincevent wrote:
Maybe the easier is to start without any inertia for testing, then adding it to fit what you want to have in terms of gameplay, but again, I'm not a coder.
This is the best advice. The easiest things first, when it's somewhat usable (you have a few levels, enemies, and your character jumps) you then start with the details. Googling "platorm game programming physics", the first link I found was this thread, with a link to physics in Sonic games among other useful stuff.
Editor
Joined: 3/10/2010
Posts: 899
Location: Sweden
The problem is that I already have coded something that I could pass off as a platforming engine. But it's so buggy that I can't take any pride in it. It's just annoying at this point. As such, I am looking for someone to ease my load.
Joined: 7/2/2007
Posts: 3960
You should consider finding a pre-existing engine (e.g. Unity) to handle physics for you, since it sounds like you're less interested in the details of how things work and more interested in what specifically happens. You're not going to find a programmer to just implement your ideas for you; indie game development doesn't work that way. So if you aren't happy with your own efforts, you're pretty much stuck finding pre-existing solutions that do most of what you need already, and then tweaking them until you're satisfied.
Pyrel - an open-source rewrite of the Angband roguelike game in Python.
Editor
Joined: 3/10/2010
Posts: 899
Location: Sweden
Preexisting solutions don't work with my tilemap system. Nor my animation system. And I don't trust generic physics engines for platforming code, they are too unstable.
Zarmakuizz
He/Him
Joined: 10/12/2013
Posts: 279
Location: France
henke37 wrote:
The problem is that I already have coded something that I could pass off as a platforming engine. But it's so buggy that I can't take any pride in it. It's just annoying at this point.
Show us what you did. A small video, and what is the current state of the "physics engine" : what works, what doesn't, what would you want to get that you haven't started yet. Remember that you are in the world or programming: if you come and say "I'm stuck, please do it for me", you are perceived as lazy, however if you start saying "I"m stuck here, trying that and that didn't got me this, I followed these links but these don't explain what I'm looking for. Is there anything I miss? Could you help me?" then it demonstrate people that you are trying hard, your problem becomes a challenge and helping you solving it seems worth it, anybody helping you would be entertained by the problem.
Emulator Coder, Skilled player (1141)
Joined: 5/1/2010
Posts: 1217
henke37 wrote:
Preexisting solutions don't work with my tilemap system.
How complicated are the ground shapes? Just lines along a grid? Arbitrary lines along major axes? Arbitrary polygons? Pretty much arbitrary shapes?
Editor
Joined: 3/10/2010
Posts: 899
Location: Sweden
Right now they are just solid rectangles on a grid. I hope to at least get some sloped tiles too.
Joined: 7/2/2007
Posts: 3960
henke37 wrote:
Preexisting solutions don't work with my tilemap system. Nor my animation system. And I don't trust generic physics engines for platforming code, they are too unstable.
Oh come on. Plenty of people have made platforming games by taking advantage of pre-existing libraries or game engines. I mean, sure, if you don't want to use one, then that's your choice. But you can't discount them all out of hand as being "too unstable". As for your tilemap system, if you expect to write a game without having to rebuild the entire thing from scratch at least once, then you're either hopelessly naive or a far better programmer than you let on...and if you're the latter, then why do you need our help? I don't mean to be insulting; it's just a nigh-universal truth that nobody manages to write a game properly on their first try, not unless they have a lot of experience in writing games. Games are complex systems, with many interacting parts, and the software engineering required to make a game program that doesn't fall over under its own weight is nontrivial.
Pyrel - an open-source rewrite of the Angband roguelike game in Python.
Editor
Joined: 3/10/2010
Posts: 899
Location: Sweden
The problem is that I don't even understand what is wrong. I at least need help debugging my code. EDIT: Here is my code.
package CopperCrew.Gameplay.PlayerStates {
	import CopperCrew.Gameplay.*;
	import CopperCrew.Gameplay.Entities.*;
	import CopperCrew.Controls.*;
	
	import HTools.Math.sgn;
	
	use namespace playerInternal;
	
	public class PlayerState {
		
		protected var maxXSpeed:Number=15;
		protected var maxYSpeed:Number=Infinity;
		
		protected var runSpeed:Number=10;
		
		protected var forwardAccl:Number=0.2;
		protected var backwardAccl:Number=-2;
		protected var coastAccl:Number=-0.2;
		
		protected var gravity:Boolean=true;
		protected var gravityAccl:Number=0.8;
		
		protected var canJump:Boolean=true;
		protected var jumpImpulse:Number=17;
		
		protected var player:Player;
		
		protected var animName:String;
		protected var animLoops:Boolean=true;
		protected var autoMirror:Boolean=true;

		public function PlayerState(player:Player) {
			if(!player) throw new ArgumentError("Player can't be null!");
			this.player=player;
		}
		
		public function init():void {
			player.char.currentAnimName=animName;
			player.char.currentAnimInst.loop=animLoops;
		}
		public function deinit():void {}
		
		public function pretick():void {
			selectState();
		}
		
		public function tick():void {
			xMovement();
			yMovement();
		}
		
		protected function selectState():void {
			const absSpeed:Number=Math.abs(player.xSpeed);
			if(player.onFloor) {
				if(absSpeed<0>=runSpeed){
					player.setState("run");
				} else {
					player.setState("walk");
				}
			}
		}
		
		protected function xMovement():void {
			horizontalControl();
			
			//cap the speed
			if(player.xSpeed>maxXSpeed) { player.xSpeed=maxXSpeed; }
			else if(player.xSpeed< -maxXSpeed) { player.xSpeed= -maxXSpeed; }
			
			if(autoMirror) {
				if(player.xSpeed<0>0) {
					player.char.scaleX=1;
				}
			}
			
			player.x+=player.xSpeed;
			
			player.applyCollisions(true,false);
		}
		
		protected function yMovement():void {
			if(canJump && player.onFloor) {
				jumpControl();
			}
			
			if(gravity) {
				player.ySpeed+=gravityAccl;
			}
			
			player.onFloor=false;
			
			if(player.ySpeed>maxYSpeed) { player.ySpeed=maxYSpeed; }
			else if(player.ySpeed< -maxYSpeed) { player.ySpeed= -maxYSpeed; }
			
			player.y+=player.ySpeed;
			
			player.applyCollisions(false,true);
		}
		
		protected function horizontalControl():void {
			var xDir:int=sgn(player.xSpeed);
			
			var controlXDir:int=0;
			
			if(controls.kDown(Keys.LEFT)) {
				controlXDir--;
			}
			if(controls.kDown(Keys.RIGHT)) {
				controlXDir++;
			}
			
			if(xDir==0) xDir=controlXDir;
			
			if(controlXDir==0) {
				player.xSpeed+=xDir*coastAccl;
			} else if(controlXDir==xDir) {			
				player.xSpeed+=xDir*forwardAccl;
			} else if(controlXDir==-xDir) {
				player.xSpeed+=xDir*backwardAccl;
			}
			
			if(Math.abs(player.xSpeed)<0.1) player.xSpeed=0;
		}
		
		protected function jumpControl():void {
			var yDir:int=sgn(player.ySpeed);
			
			if(controls.kHit(Keys.JUMP)) {
				player.ySpeed-=jumpImpulse;
				if(Math.abs(player.xSpeed)<runSpeed) {
					player.setState("jump");
				} else {
					player.setState("jump");
				}
			}
		}
		
		public function get name():String { return "Base State"; }
		
		protected final function get controls():Controls { return player.controls; }
	}
}
package CopperCrew.Gameplay.Entities {
	
	import starling.display.*;
	import starling.events.*;
	
	import CopperCrew.Chars.Man.Man;
	import CopperCrew.Chars.Woman.Woman;
	import CopperCrew.Chars.Robot.Robot;
	import CopperCrew.Chars.BaseChar;
	
	import CopperCrew.Chars.Markers.*;
	
	import CopperCrew.Controls.*;
	import CopperCrew.Gameplay.PlayerStates.*;
	import CopperCrew.Gameplay.*;
	
	import HTools.Starling.*;
	import HTools.Starling.Tilemap.*;
	
	import flash.geom.Rectangle;
	import flash.display.DisplayObject;
	
	use namespace playerInternal;
	
	public class Player extends GameEntity {
		
		public var char:BaseChar;
		
		private var state:PlayerState;
		private var states:Object;
		
		playerInternal var xSpeed:Number=0;
		playerInternal var ySpeed:Number=0;
		
		playerInternal var onFloor:Boolean;

		public function Player(gameplay:Gameplay) {
			super(gameplay);
			cameraIntrest=100;
			name="player";
			
			loadPlayerChar("robot");
			
			loadStates();
			setState("idle");			
		}
		
		private function loadPlayerChar(charName:String) {
			var cl:Class={woman:Woman, man:Man, robot:Robot}[charName];
			if(!cl) throw new ArgumentError("Bad character name");
			char=new cl();
			vis=char;
		}
		
		public override function tick():void {
			state.pretick();
			state.tick();
			
			char.tick();
		}
		
		playerInternal function applyCollisions(xDir:Boolean,yDir:Boolean):void {
			const basePlatformingBox:Rectangle=currentPlatformingBox;
			var platformingBox:Rectangle=basePlatformingBox.clone();
			platformingBox.x+=x;
			platformingBox.y+=y;
			
			if(yDir) {
				checkFloor(platformingBox);
			}
			
			if(xDir) {
				checkWall(platformingBox,-1);
				checkWall(platformingBox,1);
			}
			
			x=platformingBox.x-basePlatformingBox.x;
			y=platformingBox.y-basePlatformingBox.y;
		}
		
		private function checkFloor(platBox:Rectangle):Boolean {
			const tileMap:ITileMap=gameplay.level.bgTileMap;
			const tileWidth:Number=gameplay.level.bgTileSet.tileWidth;
			const tileHeight:Number=gameplay.level.bgTileSet.tileHeight;
			
			const leftTile:int=platBox.left/tileWidth;
			const endTile:int=Math.ceil(platBox.right/tileWidth)+1;
			const tileY:int=platBox.bottom/tileHeight;
			
			var tileRect:Rectangle=new Rectangle(0,tileY*tileHeight,tileWidth,tileHeight);
			
			var hit:Boolean=false;
			
			for(var tileX:int=leftTile;tileX<endTile;tileX++) {
				var endLoop:Boolean=false;
				var tileData:TileData=tileMap.tileAt(tileX,tileY);
				if(!tileData) continue;
				
				tileRect.x=tileX*tileWidth;
				var intersection:Rectangle=tileRect.intersection(platBox);
				if(intersection.width==0) continue;
				
				var tileInfo:TileSetTileData=gameplay.level.bgTileSet.tiles[tileData.tileId];
				
				//trace("Floor collision",tileRect);
				
				var solid:Boolean=false;
				if(tileInfo.attributes.solid) solid=true;
				
				if(tileInfo.attributes.floor) {
					if(tileInfo.attributes.oneWayFloor) {
						if(ySpeed>=0) solid=true;
					} else {
						solid=true;
					}
				}
				
				if(solid) {
					
					//nudge the platBox up to one pixel above the tileRect
					platBox.y-=intersection.height;
					
					ySpeed=0;//hitting solid ground imedetly cancels the y speed
					//endLoop=true;// won't be any other hits, since only one row was scanned
					hit=true;
					onFloor=true;
					
					dispatchEventWith("floorHit");
				}
				
				
				if(endLoop) break;
			}
			
			return hit;
		}
		
		private function checkWall(platBox:Rectangle,dir:int):Boolean {
			const tileMap:ITileMap=gameplay.level.bgTileMap;
			const tileWidth:Number=gameplay.level.bgTileSet.tileWidth;
			const tileHeight:Number=gameplay.level.bgTileSet.tileHeight;
			
			const startY:int=platBox.top/tileHeight;
			const endY:int=Math.ceil(platBox.bottom/tileHeight);
			
			const tileX:int=(
				(dir==-1)?
				Math.floor(platBox.left/tileWidth):
				Math.floor(platBox.right/tileWidth)
			);
			
			var hit:Boolean=false;
			
			
			var tileRect:Rectangle=new Rectangle(tileX*tileWidth,0,tileWidth,tileHeight);
			
			for(var tileY:int=startY;tileY<endY;++tileY) {
				var endLoop:Boolean=false;
				
				var tileData:TileData=tileMap.tileAt(tileX,tileY);
				if(!tileData) continue;
				
				tileRect.y=tileY*tileHeight;
				var intersection:Rectangle=tileRect.intersection(platBox);
				if(intersection.height==0) continue;
				
				var tileInfo:TileSetTileData=gameplay.level.bgTileSet.tiles[tileData.tileId];
				
				//trace("Wall collision",tileRect);
				
				var solid:Boolean=false;
				
				if(tileInfo.attributes.solid) solid=true;
				if(tileInfo.attributes.wall) solid=true;
				
				if(solid) {
					
					if(dir==-1) {//moving left
						platBox.x+=intersection.width;
					} else {//moving right
						platBox.x-=intersection.width;
					}
					
					xSpeed=0;
					endLoop=true;// won't be any other hits, since only one row was scanned
					hit=true;
					
					dispatchEventWith("wallHit");
				}
					
					
				if(endLoop) break;
			}
			return hit;
		}
		
		playerInternal function get currentPlatformingBox():Rectangle {
			const markers:Vector.<MarkerData>=char.markers;
			
			for each(var marker:MarkerData in markers) {
				if(!marker) continue;
				if(marker.type=="Platforming") {
					return Rectangle(marker.marker);
				}
			}
			
			return null;
		}
		
		private function loadStates():void {
			states={
				"idle": new IdleState(this),
				"walk": new WalkState(this),
				"run":  new RunState(this),
				"jump": new JumpState(this),
				"fall": new FallState(this)
			};
		}
		
		public function setState(stateName:String):void {
			if(state && state.name==stateName) return;
			
			var newState:PlayerState=states[stateName];
			if(!newState) throw new ArgumentError("Invalid player state name!");
			
			if(state) {
				state.deinit();
			}
			
			state=newState;
			
			state.init();
		}
		
		playerInternal function get controls():Controls { return gameplay.controls; }
		
		public override function get debugReprensentation():flash.display.DisplayObject {
			return char.makeDebugReprensentation();
		}

	}
	
}
I don't get why I sometimes get stuck in walls and/or floors.
Player (36)
Joined: 9/11/2004
Posts: 2623
It looks like you're trying to do too much with a single object. Entities shouldn't have to directly know anything about their own collision, all of that stuff should probably live their own class. Something like:
class Entity {

private:
  MovementController m_movementController;

};

class MovementController {
public:
  void move(Direction);
  void jump();
  void update();
  Vector collisionCorrection() const;

private:
  // things like position, velocity, collisionBody, etc.
};
The next thing is, I'm willing to bet that you apply collision correction wrong. You have a strange conceptual split between "wall" and "floor" and I'm not sure how useful that is. But more importantly, you're applying the movement in stages, which is far more complicated than it needs to be, and might actually be causing you some grief in the form of making it more difficult to debug, and introducing edge cases that don't need to exist. Anyway, break that class up. You don't need it living together, as step one. Then test each part independently (now that it's easier.) Combine some things that should be happening simultaneously (like xmovement and ymovement).
      if(controls.kDown(Keys.LEFT)) {
This should *really* come from outside of Entity. Your player shouldn't have to know about keyboard input.
Build a man a fire, warm him for a day, Set a man on fire, warm him for the rest of his life.
Site Admin, Skilled player (1236)
Joined: 4/17/2010
Posts: 11272
Location: RU
OmnipotentEntity wrote:
class OmnipotentEntity {
Warning: When making decisions, I try to collect as much data as possible before actually deciding. I try to abstract away and see the principles behind real world events and people's opinions. I try to generalize them and turn into something clear and reusable. I hate depending on unpredictable and having to make lottery guesses. Any problem can be solved by systems thinking and acting.
Banned User, Former player
Joined: 3/10/2004
Posts: 7698
Location: Finland
Is copy-pasting miles of code into your post really the only possibility? I think there are free services out there for this exact purpose, so that you don't have to embed enormous amounts of code here.
Editor
Joined: 3/10/2010
Posts: 899
Location: Sweden
I tried doing x and y movement in one go. It didn't work well. The player kept getting stuck in the ground. And I do not think that the code structure is wrong. The code is specific to the entity, so there is no other place where it belongs. And yes, I do know of pastebins, but I figured that it wasn't worth the effort here, since code pasting works fine.
Joined: 10/20/2006
Posts: 1248
You're trying to apply collision detection after the player object has already moved. I don't recommend that. First of all, you could get a speed that's higher than a wall is thick that way, and no collision wouldl ever be detected, as the player would have already successfully passed through the wall. And just overall, it's very easy to write buggy code that way. Check for collision in the area between the player's x and the new would-be-x, only then make the player actually move. Rewriting the whole collision code might not be a bad idea. Keep the old code save, and rewrite it line by line, using the old code as a guide. That forces you to think about every single line and you're almost sure to find an improvement or two. For complex movements (let's say there's also wind and whatever else to deal with), a common approach is to have a lazily named variable vecx, that's either positive or negative. You add up vecx=wind.xspeed+player.xspeed (if they are all signed), then check for collision which would adjust vecx and the speed (maybe there can be bouncy walls or whatever, or otherwise just set it to move straight into the wall and no further), only then change the player's x by simply adding vecx. Ideally, you'd pass vecx and vexy at the same time and pretend the player moves on a straight line along them. The overall idea of that approach is to move the whole system from one state that makes sense, into another state that makes sense. Never apply changes in hopes that you can still partially reverse them with a complex chain of methods. The "no problem, I'll fix it later" approach can quickly get out of hands. Applies to way more than just collision detection. If there's still a bug after all of that, and you can't find it, make the game print out (or otherwise log) all player.x, and vecx, or whatever else you need to print out until you find the actual mistake. Set player.x and player.xspeed in such a way that it pinpoints down the problem, read the code, think to yourself "vecx should be this right now", then print it out to check if that's actually true. In case it is, you know the mistake is probably somewhere after that line. Maybe there are better ways to debug, I haven't had any experience with flash at all. Another thing I've noticed: Don't name a method checkWall returning a boolean, if it does more than just checking if there's a wall. Yours seems to modify member variables.
Joined: 7/2/2007
Posts: 3960
Kuwaga wrote:
You're trying to apply collision detection after the player object has already moved. I don't recommend that. First of all, you could get a speed that's higher than a wall is thick that way, and no collision wouldl ever be detected, as the player would have already successfully passed through the wall.
While there are ways to get around this problem (continuous collision detection), as a general rule they tend to be more trouble than they're worth. You have to do stuff like sweeping the player's hitbox along its velocity vector to generate a convex polygon, then do collision detection on that, and then figure out when during the movement the collision occurred, etc. Just make certain that your top speed and minimum wall thicknesses are such that phasing through walls is impossible. Generally I feel like the Separating Axis Theorem (a.k.a. convex bounding polygons) is about as complicated as 2D collision detection should get without a really good reason. And the vast majority of games can get away with axis-aligned bounding boxes, which are really easy to work with. (Your other notes are fine; I just wanted to make a stand for the Acceptably Good solution instead of the Perfect solution)
Pyrel - an open-source rewrite of the Angband roguelike game in Python.
Banned User, Former player
Joined: 3/10/2004
Posts: 7698
Location: Finland
You could just use a ready-made middleware library. http://www.box2dflash.org/
Editor
Joined: 3/10/2010
Posts: 899
Location: Sweden
I am not a fan of "physics engines". They don't provide stable systems. And I am worried about performance when dealing with levels large enough to take more than a few seconds to cross.
Player (144)
Joined: 7/16/2009
Posts: 686
henke37 wrote:
I am not a fan of "physics engines". They don't provide stable systems.
Yes, they do.
henke37 wrote:
And I am worried about performance when dealing with levels large enough to take more than a few seconds to cross.
No need. Seriously, try using Box2D or any other physics engine. They've become as well-known as they are for a reason, you know. Just make sure you stick to the parts you need and actually use it well and you'll be fine. IMHO, if you want stability, don't use flash.