Introduction to CUGL
The purpose of this assignment is to force you to become familiar with CUGL. We have found that so many people wait until the last minute to get CUGL up and running, and as a result, the games in 4152 feel like they are a little bit behind those in 3152. The purpose of these new assignments is to prevent this from happening.
The good new is that, if you took 3152, then this assignment should look familiar to you. It has the same ship and background as ShipDemo, the first lab from that course. But instead of an enemy ship, we now have asteroids. Asteroids collide with the ship and cause it to lose health. Your job is to add some photon torpedoes so that the ship can defend itself.
As a reminder, 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.
Finally a word about scope. CUGL is a cross-platform library that can build for many different devices. But different devices have different input schemes (e.g. mobile devices do not support keyboard controls). To keep things simple for this first assignment, we will be focusing on desktop platforms only. That means you should be working in either XCode (for macOS) or Visual Studio (for Windows). Furthermore, because there are very subtle differences between the two IDEs, you need to tell us which one you used for development. This will aid with grading.
Table of Contents
Useful References
As with the first lab in 3152, a large part of this assignment will be reading documentation and learning where to look for information. We have kept this assignment simple, so you do not need to learn any of the third party libraries like box2d. But the following links are important for this assignment.
CUGL Class API
The CUGL C++ classes are the primary classes that you will use to write your game. These classes, plus a little Box2D and maybe some OpenGL should be enough for your to develop any kind of (2D mobile) game you want this semester. The API above is a Javadoc-style web page generated from the class headers.
C++ API
This website is a good collection of API documentation and tutorials. It is less official than cppreference.com, but contains a lot more information. It is my goto when I need to look up something about a standard library class.
SDL API
This API consists of all of the functions and structs (classes without methods) provided by SDL. For the most part you should not need to program in SDL. We have wrapped the most important features into CUGL classes for you. If you are an Android developer, it does help two know the JNI extensions. But since this assignment is desktop only, this is not relevant here.
Project Files
Download: ShipLab.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 can program in XCode, Visual Studio, or Android Studio. For this assignment, you will only be using XCode or Visual Studio. See the engine documentation for how to build and run on your desired platform.
Running the ShipLab
Technically, this game will run (i.e. not crash) on all platforms. However, it will not
actually do anything on mobile devices just yet. That is because the InputController
only supports keyboard controls right now. While you are welcome to add these controls
later, that is not the focus of this assignment.
The desktop, on the other hand, provides you with a little more control. As you can see you have you ship in the center and asteroids passing by on the screen. While asteroids will pass through each other, they will collide with the ship. And when they collide with the ship, they damage the ship, as you can see from the health meter. We know this is a bit unfair, but this is how the original Asteroids worked.
Right now the only thing the ship can do to defend itself is move. You move with the arrow keys. Left and right will turn the ship causing it to bank. Up arrow will thrust forward while down arrow will thrust backward. There is no friction in space, so the ship will not slow down on its only. The only way to slow the ship down is to provide a thrust in the opposite direction. While this can make the ship hard to control at times, this is once again how things worked in the original Asteroids.
Finally, you will notice that this game supports wrap-around. When either the ship or the asteroid goes off one edge, it comes back around the opposite edge. This has important ramifications for how objects are moved, drawn, and collided with.
Your goal in this assignemnt is to add photons to defend the ship. This includes
- Making a fresh C++ class for the photons
- Drawing the photons on screen
- Deleting photons when they go too far
- Writing code to detect photon-asteroid collisions
- Breaking up asteroids or deleting them on a collision
Project Structure
We will talk about the exact architecture of CUGL in a future lecture. But as we have not had this lecture yet, we will go over the basics.
The Root Classes
The file main.cpp
is the main entry point for this project. For those of you from
the introductory class, it is the analogue of DesktopLauncher
. It sets the name of
your application and the scren size. Since you do not need to change either of those,
you will not be making any changes to this file. As far as we are concerned, the
main entry point is the root class ShipApp
.
Th class ShipApp
is a subclass of Application.
It has methods for starting for starting the game, running the animation loop, and shutting
the game down. However, it does not do the real work of running the game. It only
creates three classes, as shown in the dependency graph below.
An arrow from one class to another means that the first class makes a reference to the second class in its code. Throughout this assignment you will note that there are no cycles in the dependency graph. Remember from 3152 that this helps keep the code manageable (though this assignment still has a lot of room for improvement).
The classes GameScene
and LoadingScene
are examples of game modes, which we
mentioned in the Mechanics Revisited lecture. A mode is
just any self-contained way of interacting with the game. A mode can be a menu screen,
an inventory screen, combat screen, or whatever. In this game are two modes are the loading
screen (LoadingScreen
) and the game (GameScreen
). Each of these is a subclass of
Scene2.
The third class is the SpriteBatch
, and those of you from 3152 should remember what that
is. This is exactly the same concept. It allows you to batch sprites together in a single
2d mesh and sends everything to the graphics card in bulk. This class is
actually part of CUGL,
so there is no custom code for this batch in this assignment.
Once again, those of you who took 3152 will remember that we wrapped SpriteBatch
in
another class called a GameCanvas
. The purpose of the GameCanvas
was to include
important information like the screen size and the orthographic projection. In CUGL,
all of that information is actually part of the Scene2
classes. That is because of
how scene graphs work. While you will not work with scene graphs in this assignment, they
will become incredibly important later.
The Game Scene
All of the other classes are associated with GameScene
as their root class.
That is, only GameScene
is responsible for instantiating the other classes
in the game, and not ShipApp
or main
. As with 3152, this code follows
a classic Model-View-Controller pattern.
The illustration above shows the dependency graph for the remaining classes in
package shipdemo
. We summarize the important classes as follows:
GameScene
As far as this assignment is concerned, GameScene
is the true root class. It initializes
the game, creating instances of all other classes. It also manages the game-loop via the
update
and render
methods. If you add new objects to your game, you will need to
modify this class.
There is an interesting side-note about the render
method. The parent class
Scene2 already has
a render
method and can handle drawing automatically. That is because the scene
has an associated scene graph, and CUGL sprite batches know how to draw these. But
in this case we have overridden the render
method in GameScene
. That is because
we are no using scene graphs. Instead, we are drawing directly to the sprite batch in
a way that students from the introductory class should be familiar with.
InputController
InputController
is a subcontroller for managing player input. It converts the input
from low-level commands to something semantically meaningful (e.g. the game verbs). This
is an incredibly important class for CUGL, because different devices support different
forms of input. Some devices have keyboards while others don’t. Similarly, some devices
have touch screens while others don’t. The purpose of this class is to abstract these
issues away from the rest of the application.
Right now, this class only supports keyboard commands, and hence only works on the desktop. Mobile support is beyond the scope of this assignment. Indeed, you should never need to modify this class.
CollisionController
This is a subcontroller for handling ship-to-asteroid collisions. Eventually you will
extend this to include ship-to-photon collisions as well. Collisions and physics should
always be in a dedicate controller. A model class (like Ship
) should never manage its
own collisions, since collisions are a binary relationship between (potentially) multiple
classes.
Because this is one of the classes that you need to modify, we should talk about its
structure. You will notice that it has a init
method in addition to the constructor.
Indeed, the constructor does not do anything, init
does all of the work. Why is that?
Notice that we do not use a pointer to reference the collision. That is because this
is an non-dynamic object. We talk about this in the C++ classes
lecture. The object was not created with new
. So exactly when was it created? Since
it is a field of GameScene
, it was created exactly the moment GameScene
was created.
And GameScene
is a non-dynamic field of ShipLab
, it was created the instance that
ShipLab
was created. And ShipLab
is a stack-based object created on the first line
of main.cpp
. So in other words, this object was created at the very beginning.
More important, it was created before we actually knew what our window size was. All
mobile devices are different, so you do not know your window size until the game starts
up. And that window size is what we need to initialize CollisionController
(to support
wrap-around). So we need to initialize CollisionController after the object is actually
created. This is quite common in games, and indeed is the primary programming pattern for
Objective-C, the “native” programming language for iOS. This is the reason why almost all
CUGL classes have constructors that do nothing, but init
methods that do the proper work
of a constructor. The only exceptions are math classes (vectors, matrices) which are
designed to be fast and lightweight.
Of course all of this could be avoided if we just used a pointer and delayed allocating
CollisionController
until we needed it. But this is unnecessary, and doing it this
way allows us to eliminate pointer overhead. This is very common with controllers.
With that said, you should feel free to dynamically allocate controllers in your game
project if this makes sense.
As one last bit of warning, remember that non-dynamic objects use a period instead of an arrow to reference their fields and methods. This can trip you up a bit in this assignment as you will be combining objects that use pointers with those that do not.
Ship
This class represents a ship. It has spatial information like position, velocity, and rotation. It is a fairly lightweight model. Many of the methods that you would expect for a Ship object to have are actually managed by the controllers.
This model class is different from the collision controller in that we dynamically
allocate it. But you will notice that we do not use the keyword new
. We do not
use raw pointers in this class if we can help it. The use of new
leads to memory
leaks and memory leaks are very, very bad.
Instead, if you dynamically allocate an object, you should use a shared pointer. Let’s look at this line of code:
_ship = std::make_shared<Ship>(getSize()/2, _constants->get("ship"));
The type of _ship
is std::shared_ptr<Ship>
. For most intents and purposes is works
the same as a Ship*
. You can pass it around by reference, you can dereference it,
and you access fields and methods with an arrow. In addition, the std::make_shared<Ship>()
is a replacement for new Ship()
. The arguments work exactly the same and it calls exactly
the same constructor. The only difference is the return type.
The reason to use a shared pointer is because of what happens when you set it to nullptr
.
Unlike raw pointers, shared pointers support garbage collection and C++ will delete them
immediately. We talk about this in the
C++ memory lecture. The performance hit for
using a shared pointer over a raw pointer is so slight that in only matters in the most
high performance calculations (e.g. things that CUGL does in the background). So you
should always use a shared pointer in place of new
AsteroidSet
This class is a set that stores all the active asteroids. While the asteroids are themselves represented as shared pointers (because they are dynamically allocated as they are created), the set itself is not. The set contains information common to all asteroids, such as their mass, the amount of damage they cause, and the texture used to draw them.
The asteroid set also has two sets: a pending and a current. That is because we will want to add new asteroids to the set at the same time we are currently looping over the asteroid set. It should be pretty obvious why it is a bad idea to add things to a set while we are looping over it (though deleting from such a set is okay). The pending set allows us to queue up added asteroids, and add them to the system when it is safe to do so.
The other thing to notice about asteroid code (particularly in CollisionController
) is
that we make heavy usage of C++ iterators.
You should look online for tutorials on exactly how they work. They return a (raw)
pointer to a memory location to an object. That means if you are using an iterator on
a set of smart pointers, you have to dereference the iterator once to get the smart
pointer, and a second time to get the fields or methods of the smart pointer.
Why do we do this? First of all, because iterators are super fast. But more importantly, it makes deleting items in a loop much, much easier. And that is something that we do a lot in games.
Other Features
That is an overview of the classes we have provided. However there are some CUGL and C++ features you should be familiar with.
Asset Management
Those of you from the introductory course may remember that we added an extension to LibGDX to support asset loading by JSON files. You specified all of the assets in a JSON file and the asset loader automatically loaded them for you. This asset loader was filled by the loading screen, and all assets where accessed by their JSON key.
This extension was actually copied from CUGL, where we first implemented this idea.
If you look at the assets folder you will see several JSON files. The file loading.json
is to bootstrap the game with minimal assets for the loading screen. The file assets.json
defines all the assets used by the game (including some we have not used just yet). And
the file constants.json
is an extra set of constants for defining how ships and asteroids
work. Read the descriptions of those classes to see what happens when you modify this
file.
SpriteBatch Drawing
The sprite batch is the primary graphics pipeline for 2d games. While CUGL supports 3d graphics (and we have had 3d games in the past), you are much more on your own there. We have not provided a preconstructed 3d graphics pipeline (though we will talk about this in a later lecture).
You start drawing by calling begin
and finish it by calling end
. True to its name,
all drawing calls are batched until end
is called, at which point they are all processed
at once. There are a lot of possibly drawing calls supported and you should look at
the SpriteBatch
documentation to see all of them. For the most part this assignment only needs the ability to
draw a texture within a rectangle, like we do with the background image.
However, there are two exceptions. The first is a
SpriteSheet.
A sprite sheet works like the FilmStrip
class from the introductory course. It breaks
an image into multiple frames and allows the user to animate these frames. While there
is only one asteroid texture, there are multiple asteroid sprite sheets, one for each
asteroid. That is because each asteroid could be at a different animation frame. But since
they all share the same texture, this class is very lightweight.
In addition, the sprite sheet handles its own drawing. By that we mean you pass the sprite batch to the sprite sheet instead of the other way around. We take this approach when we want to extend the capability of the sprite batch. The sprite batch does enough as it is, and does not need a new method every time we think of a new drawing type.
The other exception is drawing text. As some of you have heard me rant, working with fonts and drawing text is incredibly complicated. But fortunately CUGL handles all of these details for you. There is a class called TextLayout. You assign this class a font and the text to draw, and it creates all of the drawing information for you. You then pass this to the sprite batch to draw. We have done this for the health meter. Note that we change the color to black before drawing. That is because the default color for fonts and all other objects is white in order to support color tinting.
The other thing to keep in mind about a text layout is that it is a layout. You can
specify the size of the box and use this to create multiline text. But it also means that
you must call layout
whenever you change the text. If you do not do this, nothing
will be drawn.
Object Allocation
One of the things you learn from this assignment is that sometimes you want a non-dynamic object and sometimes you want a dynamic object. And whenever you want a dynamic object, you typically want a shared pointer. That is the reason for a particular pattern in use by all CUGL classes outside of the math package.
If you want a non-dynamic object, you initialize it by calling init
. This allows you
to initialize an object long after it is actually created. However, std::make_shared
calls the (useless) constructor and not any of the init
methods. Thereforce, if you
want a dynamically allocated shared pointer, we have provided a static method called
alloc
. Just like std::make_shared
corresponds to a constructor call, each alloc
method corresponds to an init
with the same name and arguments. For example, to
dynamically allocate a new sprite batch, you would call
std::shared_ptr<SpriteBatch> batch = SpriteBatch::alloc();
Once again, this init-alloc pattern comes from Objective-C, the native language for iOS. But it is in general a nature pattern for programming games. With that said, you are not require to construct your own classes using this pattern. You should use what is most natural.
The const
Keyword
If you have never programmed in C++ before, you maybe confused by the word const
that appears everywhere. This is type feature that allows you to specify that
something cannot be modified. But you don’t just apply it to variables. You
can apply it to methods as well.
It is very easy to get yourself in trouble if you do not know how to use const
properly. The type system will complain that you have a const
violation because
you are secretly modifying something you promised that you would not. This is known
as “const hell.” The easiest way to prevent you from falling into this trap is not to
use const
for now. Just drop the keyword.
The most counter-intuitive use of const
is with shared pointers. Consider this method
bool resolveCollision(const std::shared_ptr<Ship>& ship, AsteroidSet& ast);
We do not have a const
before the reference to the asteroid set, as we may want to
modify the set. But why then do we have a const
before the ship? Surely we want to
modify that is well. Well, we can modify the ship. The const
just says we cannot
modify the pointer to point to a different ship; modifying the contents of the ship
is just fine.
Why do we do this? If you notice, we also add an & sign to the pointer, which means
we are passing the pointer by reference. It might seem redundant to pass a pointer by
reference. But what we are saying is that we don’t want this function to copy the
shared pointer, as copying a shared pointer asserts ownership,
and that adds expensive overhead we do not want. But we also do not want to allow the
function to destroy the shared pointer itself, hence the const
. Indeed, this approach
to shared pointers is considered best practice even outside of CUGL.
IMPORTANT
We discovered in class there is a typo in the code documentation. It says that the ship angle starts in the east and is measured in radians. This is wrong. The ship angle starts facing to the north and is measured in degrees.
Instructions
Throughout this assignment you will modify three files
- SLPhotonSet.cpp and its header
- SLCollisionController.cpp and its header
- SLGameScene.cpp and its header
The last two have a lot of code already written, and you can use the code there to
help you figure out what to do. The first two files are completely blank, however.
It is up to you to learn the C++ to fill them. Fortunately, PhotonSet
and AsteroidSet
set have a lot in common.
1. Create PhotonSet
and Photon
As we hinted, you want to make a photon set and you will use asteroid set to guide how you do it. The photon set and the asteroid set should have the following things in common:
PhotonSet
is a class with anunordered_set
that contains individual photonsPhoton
should be an inner class ofPhotonSet
PhotonSet
should contain shared pointers ofPhoton
objects- You should initialize
PhotonSet
with the photons constants from the JSON file
However, they have the following differences:
PhotonSet
only needs one set, as we will never add a photon while looping over them.Photon
objects do not need a sprite sheet as they are not animated
The class PhotonSet
should contain the attributes that all photons have in common:
speed
: The initial speed of a photon (to multiply by a direction vector)mass
: The mass of a photonradius
: The radius of a basic photonmaxage
: The lifetime (in animation frames) of a photon before deletion
All of these are defined in the JSON file except radius. The radius will be defined by the size of the texture (which we will handle in the next step).
The class Photon
should have the following attributes:
position
: The photon positionvelocity
: The photon velocityscale
: The drawing scale to vary sizeage
: The current photon age (start at 0)maxage
: Another reference tomaxage
so the photon can compare.
For the most part, this is a simplified version of how asteroids work. The only issue is the age, which we can ignore for now. We do not care if the attributes are public or private – that is up to you.
We will not worry about making new photons just yet. But you should initialize the
photon set in GameScene
using the contents of the JSON file. This will require a
modification to both GameScene
and its header (to add a new attribute) . Again, look
to the asteroid set for guidance.
Since nothing is drawn yet, you will need to use print statements to debug you code and
make sure that this initialization has gone correctly. The function CULog
will help
you here. This is a cross-platform version of printf
that was covered in the
C++ basics lecture.
2. Generate Photon
objects
You should add a method to spawn new photons to PhotonSet
. Unlike asteroid, there is
only one type of photon. But when the asteroid is spawned, you will need to know the
location, velocity, and angle of the ship.
You will create the photon at the same location as the ship (because the ship is firing it). For the velocity, create a unit vector with the same angle as the ship and multiply this by the default photon speed. However you should also add the ship velocity to this vector so that the ship does not run into its own photons. New photons should start out with an age of 0 and a scale of 1.5.
You will also need to add update methods to the photon set and the photon. The photon set update should loop through the photons, calling the update method for each. Futhermore, if any photon reaches its max age, you should delete the photon. Once again, use the technique for deleting items in a loop.
The update method for an single photon should do four things:
- Add the velocity to the position
- Wrap the photon if it goes out of bounds
- Advance the age by 1
- Update the scale to be 1.5 - 1.5*age/maxage
Once again, the asteroid set is a valuable reference.
Add calls to the update method to GameScene
. You should also add code to fire a
photon. Fire a photon only if the user presses the fire key (space) and the ship
is ready to fire.
Once again, you are not ready to draw anything. Use CULog
to test that photons are
successfully being created when you press fire. You should also make sure they are being
deleted correctly.
3. Draw the Photons
Now it is time to draw the photons. This is both easier and harder than the draw code in
asteroid set. Add a texture attribute to PhotonSet
and assign the appropriate texture
using the asset manager in GameScene
. There is no sprite sheet to worry about since
photons are not animated. Individual photons do not need a reference to this texture
either. But you should use this texture to set the radius of the photons. The radius is
half of the maximum of the width and height of the texture.
When you draw the texture you will need to scale the image to match the photon scale. You should use an Affine2 transform to scale and position the image of each photon. You will want to use the SpriteBatch method that allows you to specify an origin. The origin should be the center of the texture. Otherwise, the sprite batch will put the bottom left corner of the texture at the position you specify.
You should also remember to support wrap-around by drawing an additional image whenever a photon crosses an edge of the screen. Again, look at asteroid set for guidance. You need to wrap when the edge of the photon crosses the edge, not just the center. That is why we need to add the radius. Remember that the true radius of a photon is the basic radius times the scale.
When you are done you should be able to visually inspect your work by firing your weapon. It is probably a good idea to test the basic code before you implement wrap around. But once you do have wrap-around, fire at the edge of the screen to verify it works.
4. Add a Sound Effect
We want to give some extra feel to the weapons system with a sound effect. There are two appropriate sounds effects for the photo: “laser” or “fusion”. Pick the one you like best and use AudioEngine to play the sound. There is an example for the sprite-asteroid collision that you can use as a guide.
For the most part, the audio engine is straight forward. The only thing that may be confusing is the key (particularly if you did not take the introductory course). Every sound must have a key. This key uniquely identifies that sound instance so that you cancel it later or make changes. In fact, you can only have one sound at a time with a given key, so playing a sound with the same key will cancel the first one. This is a way to keep from overloading your sound card.
5. Add Collision Code
It is now time to add collision code to support photons. You should make a new method
in CollisionController
that checks for photon-to-asteroid collisions. Detecting a
collision is easy; just look at how we check for ship to asteroid collisions (everything
is a circle). The only issue is what to do when a collision happens.
The answer depends on the type of the asteroid. In all cases, both the asteroid and the photon are destroyed. You should remove them from the appropriate set. But if the type of the asteroid is any value greater than 1, you should replace the asteroid with three asteroids of one less type (and hence smaller size). The photon is essentially breaking the asteroid apart.
When you generate the three asteroids, they should all start with the same position as the old asteroid (asteroids do not collide with each other). But they should have three different velocities. The first should have the same speed as the old asteroid, but the direction of the photon. The other two should be 120 degree rotations of this velocity vector. This is shown in the pictures below (but remember the asteroids start in the same position).
Collision | Aftermath |
Remember to call this method in GameScene
so that you actually do check for collisions.
Finally, you should play a sound effect if there is a photon-asteroid collision. We
recommend the “blast” sound effect.
7. End the Game
Congratulations! The ship can now defend itself. But to properly make this into a game, we need to add an ending. The game ends when the ship reaches 0 health or all asteroids are destroyed. The first is a loss and the second is a win.
When the game ends, all animation should halt. And you should display a message on the screen. This message should be centered on the screen and tell the player whether or not they won or lost. To do this, you will use a TextLayout object, just like we used for the health meter.
But here is the catch: fonts are fixed to a certain size. We will talk about why this is in a future lecture. But the current font is too small for a win-loss message. We want it three times the size. However, you are forbidden from loading a font of a larger size. Instead you should scale the text by a factor of 3 before drawing it. We also want you to color the text. It should be green for a win and red for a loss.
If the user wants to play again, they should be able to do so by pressing the R key.
In fact, the R key means that the player can reset the game at any time, even while
still playing. To properly support this, you need to add (re)initialization code to
the reset
method in GameScene
to clear out any stray photons.
Once you complete this you are done with the assignment.
Submission
Due: Thu, Feb 02 at 11:59 PM
This is reasonable amount of work for a masters student to complete in a week. In fact, if you already know C++, you could probably complete this in a long evening (though do not wait until the night before!). But we think this is an excellent tutorial to show you the basics of the engine. And in our experience you will probably spend more time just getting the engine to compile for the first time, as you learn XCode or Visual Studio.
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:
SLGameScene.h
SLGameScene.cpp
SLPhotonSet.h
SLPhotonSet.cpp
SLCollisionController.h
SLCollisionController.cpp
readme.txt
The file readme.txt
should tell us if you worked in Visual Studio or in XCode. That will
help us in grading your work. In addition it should describe (very briefly) how you
handled each task and where in the code it was handled.
Submit this zip file as lab1code.zip
to CMS