Introduction

So you have this great idea for a game. It could be an RPG top-down adventure, a side-scroller action, a puzzle game, or even something else! So get to planning and designing how your game will run and you come across the graphics area. You feel you need something more powerful than the simple scale() rotate() commands that effect only the pixel rendering of images but not their positional rendering in Blitzmax, but you become stuck. You have world scaling and rotation working in most cases, but you’re codes a mess and it isn’t elegant and easy to maintain and has just become ‘throw-away’ code just to get this game done. You don’t fully understand why some of it is working and why other parts fail to render correctly when you change a seemingly insignificant setting…like setting origins.

I’m sure that paints a pretty accurate picture for many beginning game developers and I’m sure we’ve all been there in some form or another. I know I’ve been there several times myself. At about the 3rd or 4th time I came across this issue I decided it was time to write a simple 2D camera library so I can just be done with it. It later evolved into a little more sophisticated 2D drawing framework, which allowed asset management, layering, and easy animation loading, but that’s beyond the scope of this tutorial.

This tutorial is designed to help you come to grips with doing transformations on your game world to allow for much more flexible gameplay and design. Why should you have to worry about complicated expressions to draw your images everytime you want to zoom into your level or rotate? With the added bonus of Blitzmax’s OOP style this can become a one time deal and then re-use this code easily and without worry everytime you want to add in this functionality 🙂 . By the end of this tutorial series you will have a functional two dimensional camera class you can use in your games for easy zooming, rotating, and moving. In Tutorial 1 you will have a very basic camera that can move around in a 2D world and render your images with you having to worry about displacement or origins.

So lets move onto the basics…

Structure

Before any decently sized project it’s always a good idea to plan what your project will do, how it will do it, and sometimes even how you will maintain it. So with that in mind:

  1. The camera class will be able to move/translate/rotate with simple method
  2. There can only be one camera class in use for the entire life of the program (a singleton)
  3. The camera class will wrap the normal blitzmax drawing api with its own to allow for easy use with blitzmax applications
  4. To facilitate easy camera manipulation we will need a wrapper for the TImage class and a class to help us with draw settings such as alpha, scale, and rotation
  5. So the programmer won’t have to mess with any confusing equations to get their mouse position and vice-versa this camera class will handle getting correct position for you from global to local and local to global with method calls.
  6. For simplicity’s sake this camera system will not natively support drawing of lines,ovals, or rectangles. But because the code is maintainable this shouldn’t be difficult to add when needed.

So from the above you can see we’ll have atleast 3 different classes powering this 2D camera system. In addition to that our camera class will be a singleton class.

[blitzmax] SuperStrict

Import “TImage2D.bmx”

‘ A camera 2D class ‘ for blitzmax 2D rendering ‘ notes: this is a singleton Type TCamera2D

Global camera:TCamera2D = New TCamera2D

‘ singleton class setup Method New() camera = Self End Method

‘ factory for getting an instance Function getInstance:TCamera2D() If camera = Null Then camera = New TCamera2D End If Return camera End Function

End Type [/blitzmax]

With the getInstance() function you can get the same instance of the camera object each time. You should ideally never call ‘New TCamera2D' in your program except maybe at the beginning. Now we are going to leave our camera class alone for a while. We’ll be making the two other classes first since the camera class will need to use them to draw anything. Since this is a tutorial about 2D camera systems, you can skip the explanations below for TImage2D and TRenderState and just copy/paste the code if you are comfortable with Blitzmax’s drawing api and go ahead to translations on page 3 to begin the real work :).

Drawing Images

So to draw an image with our camera class we are going to need several things. We will need an Image class that will wrap TImage to give us a bit more functionality. This functionality will allow each image to hold its own alpha, rotation, scale, and blend mode. We will need a RenderState object that will manipulate the draw settings such as alpha,scale,rotation,blend per image.

Design Tip: When you start having a huge list of fields inside of a type that you constantly need to manage this should start signaling that you probably need a separate type to handle this, as is the case here with RenderState and Image2D

Below is the TImage2D class that simply wraps TImage with the functionality needed to work with the camera class. Notice that it is dependent on the RenderState class as that is what it uses to setup draw settings. Nothing too special here.

TImage2D.bmx

[blitzmax2] SuperStrict

Import “TRenderState.bmx” ‘ image class wraps TImage

Type TImage2D

Field image:TImage Field drawSetting:TRenderState Field totalFrames:Int Field currentFrame:Int

Method New() drawSetting = New TRenderState currentFrame = 0; End Method

Function LoadImage:TImage2D(path:String) Local img2D:TImage2D = New TImage2D img2D.image = .LoadImage(path) img2D.totalFrames = 1 Return img2D End Function

Function LoadAnimImage:TImage2D(path:String , width:Int , height:Int , first:Int , total:Int) Local img2D:TImage2D = New TImage2D img2D.image = .LoadAnimImage(path , width , height , first , total) img2D.totalFrames = total img2D.currentFrame = 0 Return img2D End Function

Method Draw(x:Float , y:Float) DrawImage image , x , y , currentFrame End Method

End Type [/blitzmax2]

By the way, if you look at the LoadAnimImage and LoadImage functions you’ll notice I call those functions with a ‘.' in front of it. This tells blitzmax to call the original loadimage/anim functions and not the ones I just currently defined.

Below is the TRenderState. If you’ve used Blitzmax for a while you’ll understand what setalpha, setscale, and setrotation do. If not, then please look them up in the blitzmax documentation as I think it explains it enough not to warrant a full explanation here.

TRenderState.bmx

[blitzmax2] SuperStrict

‘ renderstate used by TCamera2D and TImage2D Type TRenderState

‘ renderstate stack Global rStateStack:TList = CreateList()

‘ color Field red:Int Field green:Int Field blue:Int ‘ opacity Field alpha:Float ‘ scale values Field scalex:Float Field scaley:Float ‘angle in degrees for rotation Field rotation:Float ‘ alphablend/lightblend/solidblend/etc Field blendMode:Int

‘ default values for a new render state Method New() red = 255 green = 255 blue = 255 alpha = 1.0 scalex = 1.0 scaley = 1.0 rotation = 0.0 blendMode = ALPHABLEND End Method

‘ render state manipulation======= Function PushState(rState:TRenderState) If rState <> Null Then rStateStack.addLast(rState) rstate.enableState() End If End Function

‘ pop a renderstate off the top of the stack. Function Pop:TRenderState() If Not rStateStack.IsEmpty() Then Local rState:TRenderState = TRenderState(rStateStack.removeLast() ) If Not rstateStack.IsEmpty() Then Local curState:TRenderState = TRenderState(rStateStack.last() ) curState.enableState() Else New TRenderState.enableState() End If Return rstate Else New TRenderState.enableState() End If Return Null End Function

Method enableState() .SetBlend blendMode .SetColor red , green , blue .SetAlpha alpha .SetScale scalex , scaley .SetRotation rotation End Method ‘================================== ‘—–setters———- Method setRed(r:Int) red = r End Method

Method setGreen(g:Int) green = g End Method

Method setBlue(b:Int) blue = b End Method

Method setRGB(r:Int , g:Int , b:Int) red = r green = g blue = b End Method

Method SetRotation(angle:Float) rotation = angle End Method

Method SetAlpha(a:Float) alpha = a End Method

Method SetScale(x:Float , y:Float) scalex = x scaley = y End Method

Method setScaleX(x:Float) scalex = x End Method

Method setScaleY(y:Float) scaley = y End Method

‘———getters———— Method getRed:Int() Return red End Method

Method getGreen:Int() Return green End Method

Method getBlue:Int() Return blue End Method

Method GetRotation:Float() Return rotation End Method

Method GetAlpha:Float() Return alpha End Method

Method getScaleX:Float() Return scalex End Method

Method getScaley:Float() Return scaley End Method

Method GetScale(x:Float Var , y:Float Var) x = scalex y = scaley End Method

End Type

[/blitzmax2]

Now with our framework in place we can begin talking about translations…

Translation

I’ll begin by explaining what a translation is. A translation is moving every point in a body or area a certain distance and direction while maintaining all the relative distances between the points. It’s not the whole definition, but it works for me. In our camera class we’ll be translating just the origin. Why just the origin? Well it sure beats translating every object we try to draw, and its something blitzmax supports so…why not? In further tutorials we’ll be translating objects for more complex camera operations but for this one it is not required.

Open your TCamera2D.bmx file as we’ll start adding new methods to it. We’ll first add the basic drawing command.

[blitzmax2] Method DrawImage(image:TImage2D , x:Float , y:Float) TRenderState.PushState(image.drawSetting) image.Draw(x , y) TRenderState.pop() End Method [/blitzmax2]

I’ll explain the draw method a bit. Right before we call drawImage I tell the RenderState class to push this image’s drawsettings onto a stack. When this is called the drawsettings (alpha,rotation,etc) are set accordingly and the image is drawn. Once the image is finished drawing, we ‘pop' the draw setting off the top of the stack. This resets the draw settings to what the last settings were before this image was called.

What you’ll find is there is little to be done on the drawing part for translation. The translation part is dealt with more on the camera movement level. So lets do that now. I first added these two fields to the top of the camera class:

[blitzmax2] Field positionX:Float Field positionY:Float [/blitzmax2]

Then I added this method after the getInstance() function: [blitzmax2] Method Translate(x:Float , y:Float) positionX:+ x positionY:+ y SetOrigin( – positionX , – positionY) End Method [/blitzmax2]

hmmm…confused? Yea it can be counterintuitive, but you just have to sit and visualize it for a while. Let me try to explain:

Take two sheets of paper and stack them on top of each other. Now the bottom sheet is your game world and all the things in it. The top sheet is your camera lense or view into this world. The top left corner of the bottom sheet (your game world) is the origin. The top left corner of your top sheet (your camera) is the position of your camera in this world. Now slide the top sheet a bit down and to the right as if your ‘camera' wants to look at the bottom right area of your world. What happens to your origin? Well most people would say it didn’t do anything and just stayed put. Now move your bottom sheet up and to the left while not moving the top sheet. If you did it right your two sheets of paper will be in the same position as when you move the top sheet instead. What did your origin do now? It moved in the ‘opposite' direction of where you wanted to go! This is what we are doing here in blitzmax. By moving the origin in the opposite direction of where we want to go we can draw all of our images in our view correctly without having to do any weird translation moves for every image we draw. Clear as mud? Good. Moving on…

User Input

Now we have a very simple camera class that allows us to move in our 2D world. This is all well and good, but what happens when you want a player to ‘click' on something in your world? Well, if you don’t move your camera at all then MouseX() and mouseY() will work just fine. But what if your game is awesome and actually has a camera that follows your hero/projectile/villian/squid thingy? Then we need to have some way to transform our mouse coordinates to world coordinates.

Side note: From here on out whenever I mention transforming between two coordinate spaces I will refer to the camera view as screen coordinates(Global) and the world space as world coordinates (Local). The screen coordinates start at (0,0) which is the top left of your graphics window and then go to bottom right of your graphics window (width and height).

To allow for easy transformation between screen to world coordinates and vice-versa we’ll need two methods for TCamera2D. The first will be called GlobalToLocal(). This will take any screen coordinate and transform it to world coordinates. The second will be called LocalToGlobal(). This will do the opposite.

[blitzmax2] Method GlobalToLocal(x:Float Var, y:Float Var) x:+ positionX y:+ positionY End Method [/blitzmax2] [blitzmax2] Method LocalToGlobal(x:Float Var , y:Float Var) x:- positionX y:- positionY End Method [/blitzmax2]

Design Tip: As you’ll see in later tutorials I could have benefitted from making a Vector2D class that handles x/y components in a more object-oriented way. I did not do it here because 1) vector2D is a common class. Do it yourself 😉 2) Performance. If you understand the concepts well I would recommend doing a vector2D class regardless. Most 2D games don’t require these type of optimizations; unless the projects are large in scope and have alot of complex math to deal with.

Those two methods allow us to go back and forth between screen and world coordinates without issue. In further tutorials these methods will grow considerably as we start adding scaling and rotation.

Wrapping Up

That’s about all there is to simple translation for 2D. We got a decent framework for a 2D camera system up and running and it can move about in a 2D world! Hooray! Now with a simple “Import TCamera2D.bmx” you can setup your code quickly and easily to use the camera we just coded. I discussed in alot of detail how this camera system will work. I wanted to make sure the foundation was laid :). From here on out it’s nothing more than just adding modifications to our code, so hopefully the next few tutorials will be more brief.

Throughout these tutorials I’ll have an example program showing the camera class in action. Each tutorial will have some example showing the functionality we added to the camera. Hopefully, the new blitzmax users can see how a typical game would be setup. Onward with scaling!

Download: The file contains all the source code for Tutorial 1 and an example program using the camera2D system (and then some…). Enjoy!