Smooth scrolling using SetOrigin()

BlitzMax Forums/BlitzMax Tutorials/Smooth scrolling using SetOrigin()

TomToad(Posted 2010) [#1]
How to smooth scroll using SetOrigin()

You want to create a huge map fro your game, much bigger than will fit on your screen at one. The real question is, how do I scroll the game map smoothly as I move around? The old-school way would be to calculate where all the graphics go relative to the game world, figure out where the screen is relative to the game world, then translate the graphics coordinates to the screen coordinates. SetOrigin() does a lot of the work for you. By telling Max2D where the screen is, you only need to draw the graphics at their proper world coordinates, no longer needing to transform into screen coordinates.
For our example, we are going to create a simple driving game. The map will be created with the simplest of editors, MSPaint. To draw the map, create an image 320x320 pixels, with green (0,255,0) for the background. We are going to use grey (128,128,128) for the road and white (255,255,255) for the sides of the road. I find the easiest way to draw the map is to draw the sides of the road in white first, then floodfill the road with grey. We are going to set a single red (255,0,0) pixel to represent the starting point of our car, and a sort of peachy pink (255,255,128) relative to the car to show the angle it will start. go ahead and create your own map, or just use the map supplied below.

We also need a car graphic. When using angles in a program, I like to draw everything facing right. This makes the math a little easier in the game. Go ahead and create a .png at 32x32 pixels, or use the image below.

Now the first thing we need to do is to create a type for the track. We will keep the track data in an array and copy the information from the pixmap to the array. Accessing the array is much quicker than reading each pixel in the pixmap, and speed is needed when rendering the game.
Type Track
	Global Map:Int[320,320] 'Our Track

End Type

You will notice that the map is created as a Global within the Type. If there is only going to be one instance of a type within a program, I prefer to use Functions and Globals. This will prevent the need to make an instance so everything can be accessed through Track.Map[x,y] instead of having to go through the trouble of doing

Global MyTrack:Track = New Track
MyTrack.Map[x,y]

It really comes down to preference.

We will also need to create a type for the car
Type Car
	Global X:Float 'X position of car
	Global Y:Float 'Y position of car
	Global Angle:Float 'The angle of the car -0 degrees points right
	Global Image:TImage 'Our car image
	Global Speed:Float ' the current speed of the car

End Type

Car contains information for position, angle, speed, and the TImage of the car.

Now lets load everything into the array. We will create a Load() function within the Track type.
	Function Load(Filename:String)
		Local AngleX:Float 'This will be used to calculate the starting angle of the car
		Local AngleY:Float
		Local Pixmap:TPixmap = LoadPixmap(Filename) 'Load in our map image
		If Not Pixmap Then Error("Couldn't open "+Filename) 'run custom error handler
		For Local y:Int = 0 To 319 'Go through each pixel of the map
			For Local x:Int = 0 To 319
				Map[x,y] = ReadPixel(Pixmap,x,y) & $FFFFFF 'Copy the pixel color into the map
				If Map[x,y] = $FFFF80 'Is the pixel our angle indicator?
					Map[x,y] = $808080 'Change the color to grey
					AngleX = x * 32 'Store the Angle Indicator's x and y value
					AngleY = y * 32
				End If
				If Map[x,y] = $FF0000 'Is the pixel our car starting coordinates?
					Map[x,y] = $808080 'change color to grey
					Car.X = x * 32 'store the coordniates in the car type
					Car.Y = Y * 32
				End If
			Next
		Next
		Car.Angle = ATan2(AngleY-Car.Y,AngleX-Car.X) 'Calculate the angle relative to the car
		Car.Image = LoadImage("Car.png") 'load the car's image while we're here.
	End Function

The function above should be self explanitory. The Alpha is stripped from each pixel in the pixmap, and then copied into the Map array. It will also do a check for the car's starting location, and the "peachy pink" pixel pointing at the angle we wish the car to go. We also load the car's image while we are here.
Notice that if the pixmap doesn't load, we go to our own error handler. The handler simply ends any graphics mode we may be in and prints the error message.
Function Error(S:String)
	EndGraphics()
	RuntimeError(S)
	End
End Function

Now to get the program going. First, let's do a simple file requestor so we can pick the level we want to play. Then we set up the graphics mode and the we will call the Load function.
Local Filename:String = RequestFile("Map..")'Get the file to load
If Not filename Then End 'If the user presses Cancel, we will end the program
Graphics 800,600,32 'Set the graphics mode
HideMouse 'Hide the mouse
AutoMidHandle True 'Midhandle any graphics we load
Track.Load(Filename) 'Now load everything in

Next we will set up some variables that our program will need to render the screen properly
Local XO:Float 'Origin of the screen
Local YO:Float
Local XD:Int 'The Leftmost tile to draw
Local YD:Int 'The topmost tile to draw
Local Time:Int 'Using simple delta timing
Local Oldtime:Int = MilliSecs()
Local Delta:Float

XO and YO is where we will hold the screen's upper left corner position relative to the track. Note that the SetOrigin() function sets the graphics relative to the screen, which means that we need to negate XO and YO before passing it to the function.
When we draw the tiles for the track, There is no need to draw any of the tiles which are off the screen. So we will use XD and YD to represent the leftmost and topmost tile to be drawn
Time, Oldtime, and Delta are being used to implement a simple delta timing system.

Next we will set up some constants to use in the game.
Const TerminalRoad:Float = 10.0 'The fastest you can go on the road
Const TerminalWalk:Float = 8.0 'The fastest you can drive along the side of the road
Const TerminalGrass:Float = 2.0 'The fastest you can drive on the grass
Const Acceleration:Float = 10.0 'Acceleration speed in pixels per second^2
Const Turn:Float = 180.0 'Speed at which you turn in degrees per second

Local Terminal:Float = TerminalRoad 'initialize the terminal speed to the road

The constants TerminalXXXX represent the fastest you can drive on the various terrain. The speed is represented in pixels per second. Acceleration determines how quickly we can speed up. Turn is how quickly we can turn the car. Last we declare and initialize Terminal, which is the fastest speed the car can reach.
Now we can begin the game loop. Nothing complicated, just a while/wend which we can exit with pressing the escape key.
While Not KeyHit(KEY_ESCAPE)
Wend

The first thing we will do in our Game loop is draw the graphics. This is done by first calculating the offset our screen will have within the game.
	Cls
	XO = Car.X - 400 'We will try and keep the car in the middle of the screen
	YO = Car.Y - 400
	If XO < 0 Then XO = 0 'If the car is near the edge, we stop scrolling
	If YO < 0 Then YO = 0
	If XO > 320 * 32 - 800 Then XO = 320 * 32 - 800
	If YO > 320 * 32 - 600 Then YO = 320 * 32 - 600
	SetOrigin -XO,-YO 'Set the origin of the graphics

We try to keep the car in the middle of the screen by moving the screen relative to the car. If the car gets near the edge of the track, instead of drawing the blackness outside of the track, we simply stop moving the screen. Remember that we are calculating the screen position relative to our graphics, but since SetOrigin actually sets the graphics relative to the screen, we need to negate the origin before passing it to SetOrigin.
Now that we know where the screen is, we need to calculate which tiles need to be drawn
	XD = XO / 32 'calculating which tiles to draw is easy.
	YD = YO / 32 'Just divide the screen's position by the tile's dimentions

Well, that was simple, wasn't it? Actually there is one more step we need to do, find the lower right tile to be drawn. We can do that in the track's draw function. Saves us from needing to pass two extra parameters. Speaking of the track's draw() function, we need to define that now. The next snippet of code needs to go into the Track type.
	Function Draw(XD:Int, YD:Int)
		Local XE:Int = XD + 26 'The Rightmost tile to be drawn
		If XE > 319 Then XE = 319 'Bouning check
		Local YE:Int = YD + 20 'The bottommost tile to be drawn
		If YE > 319 Then YE = 319
		
		For Local Y:Int = YD To YE
			For Local X:Int = XD To XE
				SetColorRGB(Map[X,y]) 'In our simple example, the tiles are just colored squares
				DrawRect X*32-16,Y*32-16,32,32
			Next
		Next
		SetColor 255,255,255 'Don't forget to set the color back to white :)
	End Function
	
	Function SetColorRGB(Color:Int) 'Extract each color component for the SetColor function
		Local Red:Int = (Color & $FF0000) Shr 16
		Local Green:Int = (Color & $FF00) Shr 8
		Local Blue:Int = Color & $FF
		SetColor Red,Green,Blue
	End Function


Since our screen is 25 tiles wide and 19 tiles high, we just need to add that many tiles to the left-top one. We need to actually add an extra tile, because as we scroll, a 26th tile to the right and a 20th tile to the bottom will be visible. Also, if we are all the way to the right of the track or all the way to the bottom, there is a possibility for an array out-of-bounds error due to drawing the extra tile, so we need to check for the possibility.
After we find the tiles to be drawn, we loop through them and draw them in the proper place. We only need to calculate the x,y pixel values relative to the track. No need to transform them to the screen since SetOrigin() has done that for us.
We have defined a SetColorRGB() function. Since the color components are packed in a single integer, we need to extract them to pass to the SetColor command.

Now that we have finished drawing the screen, we need to draw the car. We can create this function in the Car type.
	Function draw()
		SetRotation Angle 'Set the car's angle
		DrawImage Image,x,y 'draw it
		SetRotation 0 'return the angle to 0
	End Function

Not much to it. Once again, no need to transform the coordinates to the screen since SetOrigin has done the work for us.

Now that we have defined the draw functions, let's call them from the main game loop
	Track.Draw(XD,YD)
	Car.Draw()

Simple as that. Just add a Flip, and our graphics are drawn.
Before we do Flip, let's add a spedometer to the screen
	SetOrigin 0,0 'return the origin to 0,0 so the spedometer will be drawn in the correct position
	SetColor 0,0,0 'The spedometer background will be black
	DrawOval 350,550,100,100 'draw a circle at the bottom of the screen
	SetColor 255,255,255 'The needle will be white
	Local SpedometerAngle:Float = (Car.Speed*180.0)/TerminalRoad - 180 'Calculate the needle angle based on speed
	DrawLine 400,600,Cos(SpedometerAngle)*50+400,Sin(SpedometerAngle)*50+600 'draw the needle
	Flip 'flip the graphics

First we return the screen's origin to 0,0. Then we draw a half circle background. Since the spedometer is at the bottom of the screen, we can just draw an entire circle there. Next we need to calculate the angle of the needle. The needle will go through a 180 degree range with 0 being left and 180 being right, so the formula (Car.Speed*180.0)/TerminalRoad - 180. The needle will be all the way to the right at the fastest possible speed(TerminalRoad) and all the way left at a full stop.
After drawing the needle, we flip the backbuffer.

Next we need to actually move the car. First we calculate the delta time. Delta will hold the number of seconds passed since the last loop (usually will contain a fraction of a second)
	Time = MilliSecs()
	Delta = (Time - Oldtime) / 1000.0
	OldTime = Time

All values will be multiplied by delta
Next, we need to check if the up-arrow key is pressed and accelerate the car if it is, or decelrate smoothly if not. If the down arrow key is pressed, we need to brake quickly.
	If KeyDown(KEY_UP)
		Car.Speed :+ Acceleration*Delta 'Accelerate the car
	Else If KeyDown(KEY_DOWN)
		Car.Speed :- Acceleration*Delta*2 'brake the car at double the acceleration rate
	Else
		Car.Speed :- Acceleration*Delta 'if the accelrator is not pressed, decelrate smoothly
	End If
	If Car.Speed < 0 Then Car.Speed = 0 'Car at complete stop
	If Car.Speed > Terminal Then Car.Speed = Terminal 'Car at fastest speed

Next we need to check if the left or right arrow keys are pressed and turn the car
	If KeyDown(KEY_LEFT)
		Car.Angle :- Turn*Delta
		If Car.Angle < -180.0 Then Car.Angle :+ 360.0
	End If
	If KeyDown(KEY_RIGHT)
		Car.Angle :+ Turn*Delta
		If Car.Angle >= 180 Then Car.Angle :- 360.0
	End If

Then we move the car
	Car.X :+ Cos(Car.Angle) * Car.Speed
	Car.Y :+ Sin(Car.Angle) * Car.Speed
	If Car.X < 0 Then Car.X = 0 'Don't let the car off the edge of the map
	If Car.X >= 320*32 Then Car.X = 320*32-1
	If Car.Y < 0 Then Car.Y = 0
	If Car.Y >= 320*32 Then Car.Y = 320*32-1

So far, so good. We just need to do one more thing before we are done. Check the type of terrain the car is driving on and throttle the speed accordingly.
	Local Terrain:Int = Track.Map[(Car.X+16)/32,(Car.Y+16)/32]'Get the tile the car is currently driving over
	Select Terrain 'Change the terminal speed based on the terrain the car is on
		Case $FFFFFF
			Terminal = TerminalWalk
		Case $00FF00
			Terminal = TerminalGrass
		Case $808080
			Terminal = TerminalRoad
	End Select

Now our code is complete. For reference, here is the program in it's entirety.

Run the code above, and have fun. be sure that the car.png image is in the same directory as the program. Try modifying the program by adding checkpoints, AI, oil slicks, etc...

For a bonus, here is the source to my PathMaker program. create a 320x320 pixel png with a white background. Draw a black line where you want the road to go. Pathmaker will then create a road along the path. Load the resulting pixmap in MSPaint and set your red and peachy-pink pixels, then load into the Driving program.



GW(Posted 2010) [#2]
I took your sourcecode and attached to it a neural net that trains the car to drive around the track automatically.
Because I created some custom maps and the NN is an additional include, the download is HERE

The program doesn't draw graphics by default for the sake of speed, to check up on how the NN is learning, periodically hold down the space bar.


Jaydubeww(Posted 2010) [#3]
Awesome tutorial. Ill definitely be looking at this in the future.


iaqsuk(Posted 2012) [#4]
Hi Tom Toad,
I am new to max and created couple of games in plus so far. can this code be converted to plus? lol, I need to learn max sooner or later. Great script. Thanks.


TomToad(Posted 2012) [#5]
I don't believe that Plus has a SetOrigin() command like Max does. I've never programmed in Plus, but I have used BlitzBasic portion of Blitz3D and I believe the commands are similar. However, it shouldn't be too complicated to mimic the behavior of SetOrigin. You just need a couple of variables to hold the x offset and y offset, and then add them to all the coordinates before drawing to the screen. That's basically what Max does behind the scenes anyway.


iaqsuk(Posted 2012) [#6]
After looking at the commands in max, it seems to be easier then plus especially in the keys commands but everything else seems to be very similar as far as coding. I am going to start in max since I think I got the hang of plus. Thanks for the response.

The reason I asked if it can be your script can be mimiced to plus was, my map was in dim array(xx,xx) and your script makes it easier for the map anyways.

I was planning on using a example script of a rpg game in plus with character movements for the car, but your example is way better in what I was planing on. Planning on creating a street driving game and your script seems much easier.

Couple of request help on your script.
1. the map is 320,320 and once game starts, it's creates current views of 26 tiles x, 20 tiles y. Is there any way to create the view much higher like 66 x 40 tiles higher? or create a minimap on topright corner of the whole 320,320?
2. I was looking at the Acceleration and I was trying to create a reverse speed.

I've been trying and trying to change your codes for both above and can't figure it out. Please help if you can. Thanks again.


iaqsuk(Posted 2012) [#7]
Hi TomToad, or anyone that can help me. I figured out the map to view more than 26 x 20 tiles to 96 x 90 tiles. I also almost figured out the reversing the car. Here's my code:

Const Reverse:Float = -10.0 'Acceleration speed in pixels per second^2

I added another contstant "Reverse"

I replace the original (KEY_DOWN) to (KEY_B) for brakes.

my code for (KEY-DOWN) is:

If KeyDown(KEY_DOWN)
Car.Speed :+ Reverse*Delta 'Reverse the car
Else If KeyDown(KEY_C)
Car.Speed :- Reverse*Delta*2 'brake the car at double the reverse rate
Else
Car.Speed :- Reverse*Delta 'if the reverse is not pressed, decelrate smoothly
End If
If Car.Speed < 0 Then Car.Speed = -5 'Car at complete stop
If Car.Speed > Terminal Then Car.Speed = Terminal 'Car at fastest speed

then I added a "-5" if car.speed is < 0,

and it goes reverse but does not slow down like original forward acceleration and also when the game starts the car goes slighly forward with pressing (KEY_UP).

What am I doing wrong to fix the car going forward without pressing (KEY_UP), the reverse will work for now even though it's not perfert. Anyone, Thanks.


iaqsuk(Posted 2012) [#8]
Hi all, I was able to figure out putting car in reverse that behaved like the forward, but wasn't able to figure out the same speeds for grass and sidewalks in reverse. Maybe when I have more time. It was fun learning it, but I did find a blitzplus script that is similar to above script here: http://www.blitzbasic.com/Community/posts.php?topic=55598#619100 .

I was able to create a forward & reverse option on this script as well. Hope it is useful for those who is still wanting these kinds of script. Thanks.


iaqsuk(Posted 2012) [#9]
Hi all, I found another forum with forward and reverse what I was trying to do here: http://www.blitzbasic.com/Community/posts.php?topic=87085#988284 . Thank.