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.

Geometry

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.

Bezier Curve

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.

Regular Update

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 processed
  • fixedUpdate: 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:

Deterministic Update

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.

ASIDE: If you read the documentation, you will discover a slight difference between the 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.

Path2

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.

Extrusion

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.

Control Handles

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.

Star Polygon

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.

Controls

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