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.
In addition to building shapes, this is also the first lab where you will launch your application on a mobile device. While 90% of the lab can be done on the same desktop platform that you used last time, the last step will involve porting the input commands to mobile. That means you will need an iPhone or an Android device to test your work (or else use an Android emulator on a computer). Fortunately, as we will be back in person next week, this means you can get in-person help or borrow a phone if you are having problems.
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 you 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, 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.
Adding Mobile Support
The last activity in the instructions will require you to write
code to run on a mobile device. 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, take advantage of the
fact that we are back in person to make a friend. 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.
You may also notice a version called Geometry (Sim)
. This is only if you want to run
an iPhone simulator on an Intel Mac. The iOS version has ARM specific code so it will
not run in a simulator on an Intel Mac. The iOS version is only designed for the ARM chips
on iPhones and iPads. Of course, the simulator version is not necessary if you are on an
M1 Mac, as those are also ARM machines.
To port to Android, you should start Android Studio and select Open. Navigate to the
build-android
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.
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
active 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 triangulator0
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.
Instructions
Throughout this assignment you will modify two files:
GLGameScene.cpp
and its headerGLInputController.cpp
and its header
When we completed this assignment, we did not need any more classes than this. In particular, we did not 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. That is important for calling the set
method.
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 buildScene
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. 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. To display it in
the middle of the screen, the sprite batch will need to translate it by getSize()/2
.
2. Extrude the Flattened Spline
You should use SimpleExtruder
to extrude the given flattened spline in buildScene
. 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. You should draw the polygon in black. You
should also put it in the middle of the screen (translate it by getSize()/2
). 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 buildScene
. 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 position 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.
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 and extrude it
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. The handle lines should be drawn as white and the circles (knobs) should be drawn as red. Everything should be translated by the same transform you applied to the extruded spline itself. The result will look like the image below.
3. Add a Star
We are going to add one more shape in buildScene
. At the top of GameScene
is global
variable called STAR
. Use it to create a Path2
object (it is not a spline and already
flattened). You may need to reinterpret_cast
as before.
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. 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 update
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 buildScene
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. While this is relatively easy, it is important
that you do everything in the right place. First of all, you need to add code to the
init
method of GameScene
after the call to buildScene
. This is because you
need the Poly2
objects to make the physics bodies, but you do not want to create a new
physics engine every time you rebuild the scene.
Create a new ObstacleWorld
. Gravity 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.
Now make PolygonObstacle
objects for the spline and the star. Create a copy of
their Poly2
objects and divide the objects by PHYSICS_SCALE
(this uniformly divides
all vertices by this amount). Pass this to the alloc
method and use Vec2::ZERO
as the polygon origin. 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 update
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 correspond to the spline physics object (i.e. they are transformed
uniformly) and the star corresponds to the star physics objects. For each group, create
a transform that rotated by
getAngle
and translated by getPosition()*PHYSICS_SCALE
. Now when you run the program the
star should drop to the bottom of the circle.
There is one last thing to fix with the physics. As we said, the physics should only be updated when no control point is selected with the mouse. If the user does select a control point, you should immediately delete the spline and star physics objects with clear. You should recreate them again only when the mouse is released. This means the star will reset to the center of the screen as the user drags the mouse, and it will only drop when the user lets go.
6. Add Touch Support
It is now time to add mobile support. You do not need to
modify GameScene
to do this. Instead, you need to modify InputController
to
add touch support. So all of your for this last step 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.
You should use this technique to add touch screen support. Once you complete this you are done with the assignment.
Submission
Due: Thu, Feb 10 at 11:59 PM
This assignment is a little 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.
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
GLInputController.cpp
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. You should
also 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