Geometry Tools
Now that you understand the basics of how CUGL works, it is time to get familiar with some of the more technical features. CUGL has a lot of computational geometry tools for making procedural 2D shapes. These tools are particularly important for when you are working with physics, but they are also relevant to drawing. Indeed, this lab does not have any assets other than the loading screen. Everything in the image below will be created with code.
Historically this is a long activity, the longest of the three. And we added an even new task this year to show off the features for CUGL. For that reason we have moved one of the harder tasks to Extra Credit. With that said, this extra credit is a valuable activity as it is your first chance to launch your game on a mobile device. However, if you run out of time, you can receive full credit by doing everything on the same desktop platform that you used for the first activity.
Once again, this assignment is graded in one of two ways. If you are a 4152 student, this is graded entirely on effort (did you legitimately make an attempt), and that effort will go towards your participation grade. If you are a 5152 student, you will get a more detailed numerical score.
Table of Contents
Project Files
Download: Geometry.zip
You should download the project for this assignment. This is a self-contained project just like all of the ones available from the demos page. You should pick either XCode or Visual Studio as your primary development environment. That is because these IDEs have the best debugging tools. However, if you plan to port to Android for the Extra Credit, you will need Android Studio installed as well.
Running the Geometry Lab
Once again, this game will run (i.e. not crash) on all platforms. And you can
even successfully press the play button on both iOS and Android. But beyond
that it will not do all the much. It just shows a grey screen, regardless of
the platform. That is because you are responsible for adding all the code to
GameScene.cpp
that powers the lab.
Verifying Your CUGL Version
While updating this lab, we discovered an unexpected change in Box2d. While not technically a bug in Box2d, it changed the value of something we used in our code. So we had to patch one of our classes to make it work properly. Make sure you are using version 2.5.2. If you are in doubt, always redownload CUGL, as we are keeping it up to date with our bug fixes now.
Geometry Basics
There are several concepts that are important for this assignment. To keep you from having to scour the documents to find them, we highlight them here.
Poly2
This is perhaps the most important class of all of CUGL. A Poly2
object
represents a triangulated
solid polygon. This polygon can be drawn to the screen using a
SpriteBatch.
It can also be used to define a box2d physics object. It is a general purpose
way of representing solid geometry. Anything that is not a rectangle or a
circle is a Poly2
object in CUGL.
When drawing a Poly2
object, you use the fill
command in SpriteBatch
. It
will fill the polygon with the active sprite batch color. You also need to
specify the origin. The polygon vertices do not specify where they go on the
screen, but are instead relative to an origin. When you draw the polygon on the
screen, it is placed using this origin. By default, the origin is (0,0), but
that is not necessary. It is also common to have the origin in the centroid
(i.e. center of mass) of the polygon.
There are many, many tools for creating Poly2
objects in CUGL. One of the most
useful is the class PolyFactory.
This class includes methods to construct Poly2
objects for all basic shapes
like ellipses, rounded rectangles, and capsules.
Path2
The class Poly2
originally supported both solid polygons and polylines (i.e.
the boundary of a polygon). However, this was starting to cause a lot of
problems with the architecture, so CUGL 2.1 refactored non-solid polygons into
the class Path2
. A path is essentially a sequence of line segments. Often a
path will be the boundary of a Poly2
object, but Path2
objects are not
required to be closed (i.e. first and last points are the same), and so they
can represent much more.
It is possible to draw a Path2
object with a
SpriteBatch.
However, the results are rather unsatisfactory. A Path2
objects is drawn with
lines that are one-pixel in width. Unfortunately, most mobile devices are high
DPI (what Apple calls Retina Display), so this is too small to be easily seen.
If you want to see a Path2
, it is best to give it a line stroke width. But
once you do that, it is no longer a Path2
object. It is now a solid Poly2
object. The act of giving a path a width is called extrusion. The tool
SimpleExtruder
is your primary tool for extruding a path. While there is another extruder
available, it is extremely slow (as a trade-off for being more precise) and
cannot be used at framerate.
If the Path2
object is closed, we can also “fill it in”. We do this by
triangulating the path with one of the
triangulation classes.
Right now there are two such classes: ear clipping and Delaunay (monontone
triangulation was not finished in time for the semester). Both have their
advantages and disadvantages, but their performance is pretty comparable. If
in doubt, use the Delaunay triangulator as it gives more “even” triangles.
Spline2
The class Path2
is a sequence of line segments. But what if we want to create
a curved shape? That is the purpose of Spline2
. This represents a
multi-segmented Bézier spline.
Each point on the curve consists of an anchor (the vertex itself) and two
control points (the handles). The control points are the endpoints of the
tangent line of the anchor, as shown below.
A Spline2
object cannot be drawn directly to the screen. You must flatten it
first, converting it into a Path2
object. This is done with the class
SplinePather.
Once you have flattened the Bézier spline, you can extrude it or triangulate it,
just like any other path.
PolygonObstacle
The primary physics engine for CUGL is box2d. You are free to add any physics engine you want, like Bullet. But only box2d has integrated support in the engine.
Indeed, those of you from the introductory course remember that box2d makes a
distinction between bodies (which represent position) and fixtures (which
represent a shape). Having to keep track of both of these things can be
difficult if you are not familiar with the engine. We simplified this concept
in the last 3152 programming lab by creating the Obstacle
class for you.
The Obstacle
class combines body and fixtures into a single class to make
physics substantially easier.
We have also provided support for obstacles in CUGL as part of the
cugl::physics2
namespace. CUGL uses sub-namespaces for any feature that is
not considered mandatory. There are obstacles for various shapes, such as
rectangles and circles. But for this lab you will be using the PolygonObstacle
to make arbitrary shaped physics objects. And while this sounds a little scary,
it is not. Every single Poly2
object can be converted into a PolygonObstacle
with just a few lines of code.
If you choose to use the obstacle classes, you also have to use the
ObstacleWorld
class. This is a wrapper around the b2world
class from box2d that supports
obstacles. As with the 3152 lab, this class has the added benefit of greatly
simplifying how a box2d system is created and simulated.
Deterministic Updates
Box2d is essentially a deterministic physics engine. That means that if you put in the same inputs, you will get the same outputs every time. This is great, as reproducible behavior is important for debugging (and networking). However, there is a problem. No matter how much you try, there is always going to be one part of your physics engine that variable input: the update method.
Why is this method a problem? Think about how an update loop works. As shown
below you have the update
portion that puts everything in position, and then
the render
portion that displays the objects in their new position.
The physics simulation takes place in the the update
portion of this loop. But
the physics simulation needs some notion of time. How much time do we give to
our physics simulation? Well, the obvious answer is to use the number of
seconds that have passed since the previous loop. Since our previous
animation frame was defined by the state at the start of the previous loop,
our new animation frame should account for all of the changes since that time.
But here is the problem. No matter how stable your monitor refresh is, that time will never be steady. 60 frames per second corresponds to 0.01667 seconds per update. And while you will get that sometimes, you will also get 0.017 sometimes and 0.0162 other times. These time variations are not enough to result in any visible hiccups in your animation. But they are enough to alter the results of your physics simulation.
So what do we do? We will talk about the
solution in much more
detail later in the course. But the solution is to have an alternate update
loop. We call this alternate loop the deterministic update loop. We
enable it via the method
setDeterministic.
You can see this in Line 126 of the class GLApp.cpp
in the
source code. The application uses the normal update
method
while running the loading screen, but switches to the alternate update loop
when done.
The alternate update loop is inspired (a little) by how Unity works. It is broken into three phases
preUpdate
: The start of the update loop where input and actions are processedfixedUpdate
: The middle of the loop where determinism is necessary (i.e physics)postUpdate
: The end of the loop where any final changes are made before drawing
So how does these fit together? The preUpdate
method works exactly like
update
in the normal loop. It is called once per animation frame, and the
parameter represents the amount of time that has passed since the last time it
was called. If you are ever getting confused with this new loop, you can always
write all of your code in preUpdate
and migrate code to the other methods
later. Indeed that is what we are going to ask you to do until
Task 6.
The method fixedUpdate
is interesting. It is always guaranteed to be called
at exactly
getFixedStep
microseconds. How can it do that when we do not guarantee our animation frame
rate? The answer is that it runs at a different rate that preUpdate
. It
does not run in a separate thread, as threads are messy and greatly increase
the difficulty of writing code. But if not enough time has passed since the
last time preUpdate
was called, this engine will draw without calling
fixedUpdate
at all; it will wait for the next time around. Similarly, if
too much time has passed, the engine may need to call fixedUpdate
twice
before drawing. That means that all of the configurations below are possible:
While this may seem a little confusing, the only purpose of this is to make sure
that the timestep of our physics simulation is exactly correct. Any code that
is not so time dependent does not need to be here and can be in preUpdate
instead.
It is important to understand that no input is registered between the time that
preUpdate
completes and the method render
is called. So you should never
attempt to process input during the fixedUpdate
step.
As you can see above, fixedUpdate
is a little jerky. Sometimes it runs twice
an animation frame, and sometimes it does not run at all. If all our physics
happens in this method, this can make our visuals jerky as well. That is the
purpose of the last portion of the loop. The method postUpdate
is used to
perform any motion smoothing.
Think about it this way. The length of time that your loop should simulate was
given to preUpdate
at the very beginning. The sum of all of the calls to
fixedUpdate
is guaranteed to be less than or equal to this time. The amount
of time that is left over is passed to postUpdate
. It can use that time to
predict where the objects should be at the end of the loop, smoothing out
the animation.
As you work on this activity, you will discover this is not as difficult as this
sounds. And once again, if you are in doubt, you can always do everything in
the preUpdate
portion of the loop.
fixedUpdate
and postUpdate
methods in the application and the game scene. When we designed the
deterministic loop, we felt it was unnecessary for these methods to have
parameters, as their time steps are stored as attributes in the application
class. In hindsight this was not a good idea and so we made them specific
parameters in the game scene. Look at the code in GLApp.cpp
to
see how we convert between the two.
Instructions
Throughout this main part of the assignment, you will only modify the file
GLGameScene.cpp
and its header. Unless you attempt the Extra Credit
you should not need to modify any other file. In particular, you should not need
to add any custom model classes this time. However, if you feel that you need
to add additional classes to complete the assignment, you are welcome to do so.
1. Create a Spline2 and Flatten It
At the top of GameScene
is global variable called CIRCLE
. This is an array
of floats, defining the control points of a Bézier spline. It technically
defines a circle, but we will play with that later.
You need to initialize the attribute _spline
with this data. You do that with
the
set method.
This method should be called in the init
method of GameScene
.
Technically the set method needs an array of Vec2
objects and not an array of floats. However, that is easy to do with an
reinterpret_cast.
Just remember that when you convert a float array into a Vec2
array, the array
now has half as many elements (so 13, not 26). That is important for calling
the set
method. You should also call
setClosed
to indicate that the spline connects back the beginning.
Once you have defined the spline, it is time to flatten it. For reasons that
will be made clear later, we want you to flatten it in the buildGeometry
method. That means this method should be called in init
after the spline is
initialized.
To flatten the spline you will need a SplinePather. To use this tool you call three different methods
- The set method to place the
Spline2
object in the pather tool - The calculate method to perform the flattening
- The get method to extract a
Path2
object for the spline
Why do it this way instead of placing it all in one method? That is to make the code more amenable for multithreaded programming. You can call the calculate method in a separate thread so that it does not slow down your animation. Then you get the results in the main thread.
You may or may not need to add a new attribute to GameScene
to store the
flattened results. We leave that up to you. If you want to test your work, you
can try to draw the flattened path in the render
method. The sprite batch
method to draw a path is called
outline.
But as we said before, paths are very thin. So you will get something that is
very hard to see, like the image below.
When drawing the path, remember that it is centered at the origin. The origin
of your screen is the bottom left of the corner. To draw it in the the middle
of the screen, the sprite batch method will need an offset of getSize()/2
.
2. Extrude the Flattened Spline
You should use SimpleExtruder
to extrude the given flattened spline in buildGeometry
. The process is very
similar to what you did for flattening. The only difference is that
set
does not take a pointer (for technical reasons we will not mention here) and
calculate
needs the line width. You should use the constant LINE_WIDTH
.
This time you will need a new attribute in GameScene
to store the resulting
Poly2
. This attribute will allow you to draw it. As the polygon is solid,
the draw method in sprite batch is
fill.
Again, the offset should be getSize()/2
. You should also set the sprite batch
color to black when drawing. The result will look like the image below.
2. Add Control Handles
Now it is time to add some handles to control the spline. Once again, all of
this code will be in buildGeometry
. You need to go back to the Spline2
object.
The method
getTangent
returns the two control points for each point on the curve. For any given anchor
point at position pos
, 2*pos
is the index of the right control point and
2*pos-1
is the position of the left control point. In a closed spline like
this one, the left control point of the first point is at position 2*n-1
,
where n
is the number of points. If the path were open, then there would be
no left tangent at the beginning.
For each anchor point on the spline you should first get the left and right
control points. You should then create a Path2
object that is the line between
the two. Finally, extrude into a Poly2
object with width HANDLE_WIDTH
. You
will need to store all of the these Poly2
objects somewhere, so you will need
to create a vector
or unordered_set
to hold them all (and add this as an
attribute to GameScene
).
In addition, for each control point (left or right), you should make a circular
Poly2
using
PolyFactory.
This circle should be centered at the control point and have radius KNOB_RADIUS
Finally, to test your results, you should draw them all. Drawing is the same as before, except that now you have a lot of polygons for the fill method. Objects are drawn back to front, so draw the hollow circle first, then the handles, then the knobs. The handle lines should be drawn as white and the circles (knobs) should be drawn as red. Everything should be offset by the same offset you applied to the hollow circle. The result will look like the image below.
3. Add a Star
We are going to add one more shape in buildGeometry
. At the top of GameScene
is a global variable called STAR
. Use it to create a Path2
object. It is
not a spline and already flattened, so simply using the constructor for Path2
is sufficent. You may need to reinterpret_cast
as before. Remember to halve
the number of points.
Next you want to “fill” this shape by triangulating it. Take the
earclip triangular
and follow the set
, calculate
, get
pattern as usual.
Except that when you run this program, it will crash. Why? Because the STAR
path is clockwise. All geometry shapes should be counterclockwise. The
classic computational algorithms are all predicated on this fact. Fortunately
Path2
has a
reverse
method that allows us to fix this problem easily. Call that method and try it
again.
Once you have created the triangulated Poly2
object, store it in an attribute
so you can draw it. This shape should be drawn in the center of the screen and
using the color blue. The result will look like the image below.
4. Add Input Controls
We have already created an input controller for you. This input controller responds to mouse controls on desktop. It can tell when the user presses the (left) mouse button, when the users releases it, and when user drags the mouse with the button down. You are going to use this to control the knobs.
In the preUpdate
method check for a mouse press. If so, get the position. You
will need to convert from screen coordinates (the coordinate system of the mouse)
to world coordinates (the drawing coordinates). You can do that with the method
screenToWorldCoordinates
which is part of GameScene
(inherited from Scene2
). You will also need to
remember to subtract getSize()/2
to adjust for the fact that everything is
adjusted from the origin to the center of the screen.
Once you have the adjusted position of the mouse, you should check if the
position is within KNOB_RADIUS
of a control point. If so, that control point
is selected. That point remains selected until the user releases the mouse
button at some later animation frame.
While the control point is selected, compare the current mouse position with
the previous mouse position. Add this difference to the control point. When
you update the control point, you will call
setTangent.
Make sure that symmetric
is true so that the opposite control point moves
together with it, as shown below.
However, you will not be able to see this yet. That is because while you may
be modifying the spline, you are not updating all the Poly2
objects you
created. Whenever you change a control point, you should delete all of the
Poly2
objects and call buildGeometry
again. If you do that properly, you
should now be able to see the animation.
5. Add Physics Support
It is now time to add physics support. This is not too complicated if you
were in the introductory course, as you are going to use Box2d and obstacle
classes once again. However, it is important that you do everything in the
right place. First, we want want you to create new ObstacleWorld
in the
init
method (not buildGeometry
, as you do not want to create a new
physics engine every time you rebuild the scene).
Gravity for this world should be pointing downwards, so use the value
Vec2(0,-GRAVITY)
. For the size, you want to use getSize()/PHYSICS_SCALE
.
Those of you from the introductory course will remember that one box2d unit
is 1 meter. So if we make each pixel a meter, the objects are huge. That
is why we want the box2d coordinate system to be smaller than the screen.
The next few changes happen in buildGeometry
. First of all, add a call to
clear
at the start of buildGeometry
to ensure that any previous physics objects
are destroyed.
Next, you need to make some PolygonObstacle
objects for the spline and the
star (the handles and knobs are not physics objects). Do this in buildGeometry
right after you make the appropriate Poly2
object. Create a copy of the
Poly2
objects and divide them by PHYSICS_SCALE
. We literally mean
divide, like this
Poly2 copy = _circle;
copy /= PHYSICS_SCALE;
Because of operator overloading, this will uniformly divide all vertices by
this amount, shrinking the polygon down into physics units. Pass this smaller
shape to the alloc
method of PolygonObstacle
. You will also need to set a
few properties.
- The spline body type should be
b2_staticBody
- The star body type should be
b2_dynamicBody
- The star density should be 1.
- Both objects should have position
getSize()/(2*PHYSICS_SCALE)
The last step ensures once again that the objects will centered be in the screen.
Once you have created the objects, call addObstacle to add each obstacle to the world.
To run physics, you should add the call
_world->update(timestep);
to the preUpdate
method in GameScene
. This method should only be called
when no control point is selected with the mouse. If the user is playing with
the mouse, the animation should pause.
To test your physics, you will also need to update the drawing code. The spline
and all of its handles/knobs now correspond to the spline physics object (i.e.
they are transformed uniformly) and the star corresponds to the star physics
object. To synchronize this with the drawing code, you are going to draw with an
Affine2
transform. A transform converts from one coordinate system to another. The
transform should rotated by the
getAngle
value of your obstacle. It should be translated by getPosition()*PHYSICS_SCALE
.
Once you have computed the transform, use the transform-specific version of
fill.
to draw your objects. Note that when you use this version of the fill
method
make the origin vector zero, as the offset is already part of the transform.
This was a lot to do, so run the program. If you did everything correctly, the
star should fall to the bottom of the hollow circle and stay there. If you
move the handles, this should call buildGeometry
again, which means that the
star should reset back to the center, dropping again once you release the
handle.
While not required there is a minor optimization that you could make here.
Adding obstacles to the ObstacleWorld
is a bit heavy weight. And we only
need to do that when we release the handles. So you could move the calls
to addObstacle
and clear
out of buildGeometry
and put them in preUpdate
.
But make sure to test your code thoroughly if you do this.
6. Determinize the Loop
There is only one last thing to do with this activity. You added physics. It
is time to make the physics deterministic. We covered
the basic idea above. But the implementation is
really simple. All you have to do is move the call to the ObstacleWorld
update
method from preUpdate
to fixedUpdate
. That is it!
Well, you need to still make sure that this is only called when the user has
not clicked on a handle. And since input can only be processed in preUpdate
,
this means you probably need a new attribute in the game scene to communicate
between the two methods. But other than that, easy.
We also mentioned above about how this can make the movement choppy. So you
should use postUpdate
to smooth out the animation. How to do that? First
of all, only the star needs motion smoothing at the circle does not move once
you let go of a handle.
Remember that postUpdate
is given the number of seconds since the last call
to fixedUpdate
. Motion smoothing means motion predicting. You need to
predict where the star would be if that many seconds were applied to the
simulation. Of course this can get really difficult because of collisions and
the like. But we are just trying to smooth out the motion. So we are going to
ignore any collisions, any external forces, and just assume that the star is
moving at a fixed velocity.
The star actually has two velocities. There is getAngularVelocity and getLinearVelocity. Apply these to the angle and position, respectively. But do not actually change the angle and position of the physics object. That will mess up the simulation. Instead, you only perform this calculation when you are drawing the obstacle to the sprite batch. That means where the object is drawn on screen is not quite where it is in the simulation.
Once you have finished with this, congratulations, you are done. You may officially consider yourself a master of CUGL. It gets easier from here.
Extra Credit
At some point, may of you will need to write code to run on a mobile device. And this is a really good assignment for it. This code easily runs on mobile. You just need to replace mouse controls with touch controls.
However, this assignment was already long enough as it is. So we are not requiring this step. Only do this step if you want extra credit. We will award up to 5 points extra credit for 5152 student. As 4152 students only work on these assignments pass-fail as part of their participation grade, it will be equivalent to one excused absence on a presentation day for a 4152 student.
To develop for mobile, you will need the correct computer for the correct device. Android phones can be run on either platform, but you will need to install Android Studio. iPhones require XCode and a Mac. If you do not have access to a Mac, make friends with someone who does. As a last resort, everyone in the class should be able to run an Android simulator within Android Studio (though remember about the potential emulator issues).
On the Mac, you will need switch the build target from Geometry (Mac)
to
Geometry (iOS)
. You will also need to plug in your phone or iPad. Finally,
you will need to go to Geometry (iOS)
and set the team (under
Signing & Capabilities) to “Personal Team”. This is where you set your
iOS developer account if you already have one. “Personal Team” is the
non-commercial option.
To port to Android, you should start Android Studio and select Open.
Navigate to the android
build directory and select build.gradle
. This will
initialize the project for you. Android Studio should recognize your phone
when you plug it in, provided you have
activated developer mode.
Note that if you build an Android release without a phone plugged in, it will
build for all four platforms (ARM, ARM64, x86, x86_64) instead of simply
targetting the phone. This means that compilation will take four times as long.
Add Touch Support
For the extra credit, you will replace mouse support with touch support. You do
not need to modify GameScene
to do this. Instead, you need to modify
InputController
. So all of your work for the extra credit will be confined to
that file (though you may have to add attributes to the header).
You will notice that in InputController
we get the mouse and check if it is
nullptr
. That is because it might be on a computer that does not have a mouse
(like a mobile tablet). Mobile devices use
Touchscreen
instead. For the part, most Touchscreen works exactly like a mouse. But is has
one major difference. With the mouse, we use the event to check which button is
pressed. With touch we use the event to check which finger is used.
The finger is identified by the TouchID
. While the number means nothing by
itself, this is how we separate fingers from each other when multitouch is
active. In particular, your code should only register a press if no other
fingers are down. If a new touch id appears, it is a new finger and should be
ignored. In addition, when you are checking the finger motion, you should only
recognize the motion if it is the same touch id as the initial press. When that
finger is finally released, then and only then can you respond to a new touch.
Of course this means a new set of callback functions to register, similar to the ones we created for the mouse. And sometimes the mouse and touch code does not get along. So how we tell the computer to use the right one? The answer is compiler directives. You can tell the compiler to check the platform you are compiling for and only compile code that runs on that platform.
For example, in GeometryApp
you have the following code
#ifdef CU_TOUCH_SCREEN
// Start-up basic input for loading screen (MOBILE ONLY)
Input::activate<Touchscreen>();
#else
// Start-up basic input for loading screen (DESKTOP ONLY)
Input::activate<Mouse>();
#endif
This tells the compiler to active the touch screen if you are on mobile device.
The line to active the mouse is not compiled. This is more than a simple
if-statement. The code is not skipped over in execution. The compiler does not
see it at all! Using this technique you can create code specifically for
Windows, Mac, iOS, or Android. See CUBase.h
for the various compiler options.
For the list of callback functions necessary look at the documentation for
Touchscreen.
Each event has its own listener: begin, motion, end. Create methods in
InputController
to respond to these just like we did the mouse. Remember
that adding methods requires you update the header as well as the cpp file.
You will also need to register these methods as listeners just like we did
with the mouse in the init
method.
Once you have done this, try it out on the mobile device of your choice. Congratulations, you finished the extra credit!
Submission
Due: Fri, Feb 09 at 11:59 PM
This assignment is a definitely harder than the first one. But by now you should be familiar with compiling CUGL, and have a passing understanding of C++. Once again this should be within your grasp. But start early!
When submitting the assignment do not submit the entire project. That is too large and CMS will crash under the load. Instead we want you create a zip file containing the files:
GLGameScene.h
GLGameScene.cpp
GLInputController.h
(only if completed Extra Credit)GLInputController.cpp
(only if completed Extra Credit)readme.txt
If you created new classes, you should add those as well. The file readme.txt
should tell us if you worked in Visual Studio or in XCode for the desktop
version. It should also tell us if you attempted the extra credit. If you did,
tell us what mobile device you used to test the touch code. If you used a
simulator on a computer, tell us that.
Submit this zip file as lab2code.zip
to CMS