Smooth scrolling using SetOrigin()
BlitzMax Forums/BlitzMax Tutorials/Smooth scrolling using SetOrigin()
| ||
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. |
| ||
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. |
| ||
Awesome tutorial. Ill definitely be looking at this in the future. |
| ||
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. |
| ||
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. |
| ||
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. |
| ||
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. |
| ||
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. |
| ||
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. |