Alright so you’ve made a simple 2D camera system that can move around in a 2D world. Not a big deal, it was mostly an exercise in Blitzmax’s language. Now we want to actually make something worthwhile. I assume you’ve read Part 1 of this tutorial and understand the design and concepts we laid out in our camera class. It was a bit lengthy, but I assure you the rest is pretty straightforward :). This next tutorial we will add scaling to our camera class, so lets begin.
So I’ll explain the gist of how scaling works. First, the simple case. Your origin is in the center of the screen. So what do you do? if you want to draw everything at half the scale we currently draw all we do is multiply our positions and sizes by 1/2. Seems to work right? Well, this is where most people tend to go astray and not know what to do next to make it dynamic. We have two issues. Our first issue is that our origin isn’t in the center of the screen. Our second issue is that our origin is potentially moving. Well, the second isn’t so much an issue if you deal with the first one correctly. So the solution would be to scale along an arbitrary point rather than an origin, right? Well the basic expression for such a setup is as follows:
DrawPosition = (CurrentPosition – OriginOffset) * Scale + OriginOffset
DrawPosition is our X/Y output for where to draw image in screen coordinates. CurrentPosition is where our image is in world coordinates. OriginOffset is our arbitrary origin minus our real origin(in world coordinates). So as you can see from the earlier example, if our real origin was in the middle of the screen then our arbitrary origin would be (0,0) and then it reduces down to just multiplying the position by the scale. What we are doing is translating the position so that it ‘centered’ around the real origin, and then scale it there, then bring it back to where it was originally with its new scaled value. Simple enough.
To get our camera to do this properly we are going to have to modify the draw method a bit and write a few new methods and fields. We’ll be working with TCamera2D.bmx first:
Add this field to TCamera2D [blitzmax2] Field zoomScale:Float [/blitzmax2] This’ll determine what zoom level we are currently at. Zoom of 1 is the normal drawing scale and anything less is zooming out. Anything larger than 1 is zooming in.
Add these methods below Translation(): [blitzmax2] Method Zoom(direction:Float) zoomScale:+ direction End Method [/blitzmax2] [blitzmax2] Method SetZoom(zoom:Float) zoomScale = zoom End Method [/blitzmax2] Direction simply needs to be a negative float to zoom out and a positive float to zoom in. This number will have to usually be something rather tiny, but I leave it up to the user of this camera system to determine how ‘fast’ they would like to zoom in. SetZoom is a method setup for anyone wanting to directly set the zoom level (would be good to reset to 1 if things go crazy!).
Now we have a zooming api for our camera. It’s time to make it actually do something. But in order for it to work correctly we are going to have to modify how our camera draws. So to start things off we’ll need somewhat of a temporary variable, but since we are interested a bit in performance I wouldn’t want to just make separate objects every loop that get garbage collected after a single line of code use. We can re-use this same instance for the life of the program. We’ll be making a RenderState variable for our camera.
Add these fields to the camera class: [blitzmax2] Field cameraSetting:TRenderState Field graphicsH:Float Field graphicsW:Float [/blitzmax2]
Now would be a good time to make sure all of our variables are initialized when the camera is first created. Modify your New() method to look like this: [blitzmax2] Method New() camera = Self
zoomScale = 1.0 cameraSetting = New TRenderState graphicsW = GraphicsWidth() graphicsH = GraphicsHeight() End Method [/blitzmax2]
We have to set zoomScale to 1 and our graphics variables to the window dimensions or else we wouldn’t see anything! We set the cameraSetting to a default renderstate for now. Now we have to modify our drawing method for the camera. [blitzmax2] Method DrawImage(image:TImage2D , x:Float , y:Float)
SetupCameraSetting(image) TransformPosition(x , y) ‘———————–
TRenderState.PushState(cameraSetting) image.Draw(x , y) TRenderState.pop() End Method [/blitzmax2] We are using the camera setting instead of the image’s drawsetting because we want the apply camera scaling on top of an image that could already be scaled. When you see the methods below you’ll understand. You will want to carry this concept along for rotation also.
[blitzmax2] Method SetupCameraSetting(image:TImage2D) image.drawSetting.copyTo(cameraSetting) Local scalex:Float = cameraSetting.getScaleX() _ zoomScale Local scaley:Float = cameraSetting.getScaleY() _ zoomScale cameraSetting.setScale(scalex , scaley) End Method [/blitzmax2] We just multiply the current image’s scale with our zoom level and we get the correct scale. This allows us to still scale our images indepedently of the camera. [blitzmax2] Method TransformPosition(x:Float Var , y:Float Var)
‘ world coordinates for screen ‘ center ‘ (our arbitrary origin – real origin) Local fakeOriginX:Float = positionX + graphicsW / 2.0 Local fakeOriginY:Float = positionY + graphicsH / 2.0 Local realOriginX:Float = – PositionX Local realOriginY:Float = – positionY Local originOffsetX:Float = fakeOriginX – realOriginX Local originOffsetY:Float = fakeOriginY – realOriginY
TranslatePosition(x , y , -originOffsetX , -originOffsetY ) ScalePosition(x , y) TranslatePosition(x , y , originOffsetX , originOffsetY ) End Method [/blitzmax2]
Sidenote: You would think I would be better off explicitly just plugging in that earlier expression straight into here. I can do that, and for all intents and purpsoses you probably would when you have a complete camera system. This is done to illustrate exactly what is happening to the x/y coordinates. In addition to that, adding rotation will be trivial in this setup as all we would do is insert a call to a rotation function in there instead of re-working some equation. Also note you could simplify the use of those variables by alot, but I wanted to expand it for the purposes of this tutorial.
This is our workhorse function. This function will give us the correct x,y coordinates to plot on the screen. The last 3 method calls are simply doing the expression I mentioned early in this tutorial about scaling. Below are the rest of the helper functions. I broke them down into logical pieces to make it clearer so you can observe exactly what is going on. As you can see, it really isn’t voodoo magic ;)! [blitzmax2] Method ScalePosition(x:Float Var , y:Float Var) x:_ zoomScale y:_ zoomScale End Method [/blitzmax2] [blitzmax2] Method TranslatePosition(x:Float Var , y:Float Var, tx:Int,ty:Int) x:+ tx y:+ ty End Method [/blitzmax2]
Sidenote: At this point I’m really wishing I had listened to my own advice and created a Vector2D class :|. And if this was for an actual game project I would most definitely do it to maintain consistancy.
So now you try to compile and…uh-oh! It can’t find a ‘copyTo’ definition in the drawing code. Well that’s because we haven’t written it yet….derrrr. Open up TRenderState.bmx and add this method: [blitzmax2] Method CopyTo(state:TRenderState) state.scalex = scalex state.scaley = scaley state.red = red state.green = green state.blue = blue state.alpha = alpha state.rotation = rotation state.blendMode = blendMode End Method [/blitzmax2]
Design Tip: Small thing, but has tripped me up on several occasions when done wrong. Notice the method name acts almost like a ‘verb’ to what’s in the parameter. Keeping to this convention I always know the fields on the left side of ‘copyTo’ will be copied to the right side. This can be the root of several bugs if, for instance, you have vector library and use confusing syntax while dealing with some complicated physics equations. Not fun.
Scaling is now implemented into the camera class :D! I would recommend as an exercise to go back and do a little ‘speed’ up in the camera class by simplifying a few things to really drive home the point that you undertstand what’s going on. Namely, the Transformation function with all its variables, but I would leave the function calls alone until after rotation is in.
Scaling: User Input
You might’ve noticed that after adding scaling to your camera system that the example no longer works with mouse clicking on the star. No problem. As stated before, we’ll just have to change GlobalToLocal and LocalToGlobal to fit with our addition of scaling. Here are the modified function calls. It uses the same principles used in the drawing function. To make things a bit more manageable I added an UndoTransformPosition() method. This method does the reverse of the transformation. It’ll make maintaining GlobalToLocal and LocalToGlobal a bit easier.
[blitzmax2] Method UndoTransformation(x:Float Var , y:Float Var) Local fakeOriginX:Float = positionX + graphicsW / 2.0 Local fakeOriginY:Float = positionY + graphicsH / 2.0 Local realOriginX:Float = – PositionX Local realOriginY:Float = – positionY Local originOffsetX:Float = fakeOriginX – realOriginX Local originOffsetY:Float = fakeOriginY – realOriginY
TranslatePosition(x , y , -originOffsetX , -originOffsetY ) x:/ zoomscale y:/ zoomscale TranslatePosition(x , y , originOffsetX , originOffsetY )
End Method [/blitzmax2]
Note that the reverse of the transformation requires you to divide by the zoomScale instead of multiply.
here are the new user input transformation methods [blitzmax2] Method GlobalToLocal(x:Float Var, y:Float Var) x:+ positionX y:+ positionY UndoTransformation(x,y) End Method [/blitzmax2] [blitzmax2] Method LocalToGlobal(x:Float Var , y:Float Var) TransformPosition(x , y) x:- positionX y:- positionY End Method [/blitzmax2]
With all of that in place we are now ready to take on rotation, the final frontier!
We’re on the home stretch now :D! We have only 1 more major implementation to finish and then our camera class will be feature complete.