CSCS1320 C/C++ Programming

PROJECT: FUN WITH GAMES (fwg6)

OBJECTIVE

Using your Vircon32 build and installation from fwg0, the v32c++ transpiler from fwg4, and the class hierarchy skills from fwg5, implement a simple 2D game built around polymorphism: a collection of different kinds of game objects, all handled through a base class pointer, each responding to the same function call in its own way via virtual functions. Along the way you will also try out overloading: functions that share a name, and operators (+, ==, …) taught how to work on your own classes.

Your game can be a continuation of your fwg5 game or something entirely new. The demo fwg6.cpp (see make copy) shows the ideas in miniature; do not just rename it.

As in fwg5, inheritance is single inheritance only.

BACKGROUND

The problem polymorphism solves

Suppose your game has a player, some drifting rocks, and a chaser. They are all "things that update and draw themselves", but each one does so differently. With plain inheritance you can write:

Player  me;
Drifter rock1, rock2;
Chaser  stalker;

me.update();
rock1.update();
rock2.update();
stalker.update();

…and every new kind of object means more lines in main(). What you want is one list of game objects, one loop, one call, with each object doing the right thing for what it actually is:

for (i = 0; i < NUM_ACTORS; i++)
{
    actors[i]->update();
}

That is polymorphism ("many forms"): the same call, many behaviors, the right one chosen at run time by the real type of the object.

Two ingredients

1. A base class pointer can point at a derived object. A Drifter IS-A Actor, so an Actor * is allowed to hold the address of a Drifter:

Drifter rock(200, 100, 2, 1);
Actor  *a = &rock;                  // address-of (&): a points at rock
a->update();                        // arrow (->): call through the pointer

This is where your pointer skills (address-of &, the arrow ->) pay off. Since every element of an array has to be the same type, an array of Actor * can hold the addresses of a mixed bag of derived objects:

Actor *actors[3];
actors[0] = &me;                    // a Player
actors[1] = &rock;                  // a Drifter
actors[2] = &stalker;               // a Chaser

2. Virtual functions. By default, a->update() on an Actor * calls Actor's version of update(), because that is the type the pointer claims. Mark the function virtual in the base class and C++ instead looks at what the pointer really points at:

class Actor
{
    public:
        virtual void update();      // "derived classes may replace this"
};

class Drifter : public Actor
{
    public:
        virtual void update();      // Drifter's own version: an OVERRIDE
};

Now a->update() runs Drifter::update() when a points at a Drifter, Player::update() when it points at a Player, and so on. (The word virtual is optional on the override once the base declares it, but writing it keeps your intentions clear; do so.)

Pure virtual functions and abstract base classes

Sometimes the base class cannot say anything sensible about how a function should work: what does it mean to draw() a generic Actor? Declare it pure virtual by giving it = 0 instead of a body:

class Actor
{
    public:
        virtual void update() = 0;      // pure virtual: no body here
        virtual void draw()   = 0;
};

A class with at least one pure virtual function is an abstract base class:

  • you must not create an object of it (Actor a; is an error in standard C++; v32c++ does not check this for you, so the discipline is yours, and it will be checked when your project is evaluated): it is a promise, not a thing
  • every derived class that wants to be instantiated must override all of the pure virtuals, i.e. it must keep the promise
  • you can still have pointers to it (Actor *a;), which is what makes the polymorphic array above possible

Think of the abstract base class as a contract: "anything that wants to be an Actor must know how to update() and draw()."

Overriding is not overloading

These two words sound alike and mean different things:

  • overriding: a derived class re-implements a virtual function from its base, with the same name and same parameters. Resolved at run time by the object's real type.
  • overloading: two or more functions with the same name but different parameters, in the same scope. Resolved at compile time from the arguments you pass.

Function overloading

class Actor
{
    public:
        void setPosition(int newx, int newy);       // two ints...
        void setPosition(Vec2 newpos);              // ...or one Vec2
};

actor.setPosition(10, 20);                          // picks the first
actor.setPosition(Vec2(10, 20));                    // picks the second

You have already used this without noticing: a class can have more than one constructor, as long as their parameter lists differ.

Operator overloading

Operators like + and == already work on ints. For your own classes you can teach them what to do. An operator is just a function with an odd name: operator+, operator==, operator+=, …

class Vec2
{
    private:
        int x;
        int y;

    public:
        Vec2(int newx, int newy) { x = newx; y = newy; }

        Vec2 operator+(Vec2 other)              // a + b
        {
            return Vec2(x + other.x, y + other.y);
        }

        void operator+=(Vec2 other)             // a += b
        {
            x += other.x;
            y += other.y;
        }

        int operator==(Vec2 other)              // a == b
        {
            return (x == other.x && y == other.y);
        }
};

Vec2 pos(10, 10);
Vec2 vel(2, 1);

pos += vel;                 // same as pos.operator+=(vel)
if (pos == vel) { ... }     // same as pos.operator==(vel)
Vec2 next = pos + vel;      // same as pos.operator+(vel)

Guidelines: overload an operator only when its meaning is obvious (adding two vectors with + is; making + launch a missile is not). Overloading changes how your own types are written, not what the operator can do for built-in types: 3 + 4 is unaffected.

Access control reminder (from fwg5): notice that operator+ reads other.x. That is legal because the function is a member of Vec2 and may touch the private data of any Vec2, not just its own object. Children still cannot access their parent's private parts, though.

A word about destructors

In ordinary C++ a class that is meant to be used through a base pointer also gets a virtual destructor so deleting a derived object through a base pointer cleans up correctly. Our cartridges never new/delete objects (all of ours live as local variables in main()), so we need no destructors for this project.

PROGRAM

You are to write a Vircon32 C++ game that:

  • has a player-controlled object, moved via the gamepad
  • has at least two other kinds of game object that act on their own
  • detects collisions between objects, with a meaningful result
  • keeps a score (or lives, or timer) displayed or otherwise tracked
  • has a win and/or lose condition (even if it simply resets)
  • is built around polymorphism:
    • define an abstract base class with at least one pure virtual function (for example update() and draw())
    • define at least TWO derived classes (publicly, single inheritance) that override the virtual function(s) with behavior of their own
    • keep your game objects in an array of base class pointers, holding at least two different derived types
    • use a loop to call the virtual function(s) through those pointers (->)
  • demonstrates overloading:
    • overload at least one operator for a class of yours (for example a vector or point class), and use it meaningfully in your game code
    • demonstrate function overloading with at least one set of functions (or constructors) sharing a name but differing in parameters

NOTE: the first gamepad by default is mapped to the keyboard

NOTE: the transpiler emits an empty function body for each pure virtual function, and the Vircon32 compiler reports argument "this" is not used for it. That specific warning is expected and is not held against you for the "no warning or error" line item; any other warning or error is.

NOTE: v32c++ is not a full C++ compiler. If a feature is rejected, simplify and ask in the class chat; documenting what works and what does not is valuable.

CARTRIDGE BUILD PROCESS

All of these steps can be automated with the use of the provided Makefile simply by typing make at the prompt. Use make copy to pull the demo project (fwg6.cpp, Makefile) into your repository directory, and make update to refresh the Makefile.

The build is the same as for fwg4 and fwg5: v32c++ transpiles your .cpp and generates the cartridge XML and build script (Make.sh), which compiles, assembles, converts textures and sounds, and packs the ROM.

TRANSPILE

yoursystem:~/src/SEMESTER/cprog/fwg6$ v32c++ fwg6.cpp -o fwg6.c

COMPILE

yoursystem:~/src/SEMESTER/cprog/fwg6$ compile fwg6.c -o fwg6.asm

ASSEMBLE

yoursystem:~/src/SEMESTER/cprog/fwg6$ assemble fwg6.asm -o fwg6.vbin

IMAGE CONVERSION

yoursystem:~/src/SEMESTER/cprog/fwg6$ png2vircon sprites.png -o sprites.vtex
  <binary path="fwg6.vbin" />
  <textures>
      <texture path="sprites.vtex" />
  </textures>

AUDIO CONVERSION

yoursystem:~/src/SEMESTER/cprog/fwg6$ wav2vircon soundfx.wav -o soundfx.vsnd
  <sounds>
      <sound path="soundfx.vsnd" />
  </sounds>

PACKING THE ROM

yoursystem:~/src/SEMESTER/cprog/fwg6$ packrom fwg6.xml

SUBMISSION

To be successful in this project, the following criteria (or their equivalent) must be met:

  • Project must be submit on time, by the deadline.
    • Late submissions will lose 33% credit per day, with the submission window closing on the 3rd day following the deadline.
  • Executed programs must display in a manner similar to provided output
    • output formatted, where applicable, must match that of project requirements
  • Processing must be correct based on input given and output requested
  • Output, if applicable, must be correct based on values input
  • Code must be nicely and consistently indented
  • Code must be consistently written, to strive for readability from having a consistent style throughout
  • Code must be commented
    • Any "to be implemented" comments MUST be removed
      • these "to be implemented" comments, if still present at evaluation time, will result in points being deducted.
      • Sufficient comments explaining the point of provided logic MUST be present
  • No global variables (without instructor approval), no goto statements, no calling of main()!
  • Track/version the source code in your private semester repository
  • Submit a copy of your source code to me using the submit tool by the deadline.

Do NOT track or submit generated or binary files (obj/, bin/, .c, .vbin, .v32); make submit cleans these for you.

SUBMIT TOOL USAGE

lab46:~/src/SEMESTER/cprog/fwg6$ make submit

You should get some sort of confirmation indicating successful submission if all went according to plan. If not, check for typos and or locational mismatches.

COURSE OBJECTIVES ASSESSED

Some of the rubric line items below also serve as evaluations of the course objectives listed on the course homepage:

  • Implement functional programs … that satisfy automated test requirements: the cartridge builds with no warning or error
  • Define the relationship between a variable's value and its memory address using basic pointer syntax: base class pointers holding the address (&) of derived objects, called through ->
  • Distinguish between collections of the same data type (arrays) and collections of differing data types (structures): the array of base pointers holding different derived objects
  • Construct modular code by defining functions and utilizing different methods of passing data into them: overriding virtual functions
  • Select the most appropriate control structure or data type to solve a specific, predefined logical problem: an abstract base class as the contract for your game objects
  • Utilize built-in language libraries: texture and sound use

Not every line item maps to an objective, and that is fine.

RUBRIC

I'll be evaluating the project based on the following criteria:

312:fwg6:final tally of results (312/312)
*:fwg6:code modified to meet project specifications [26/26]
*:fwg6:screenshot or video posted to class DISCORD of game running [26/26]
*:fwg6:abstract base class declares virtual and pure virtual functions [26/26]
*:fwg6:at least two derived classes override a virtual function [26/26]
*:fwg6:virtual functions invoked through base class pointers [26/26]
*:fwg6:array of base pointers holds different derived objects, driven by a loop [26/26]
*:fwg6:at least one operator overloaded in a class [26/26]
*:fwg6:overloaded operator is used meaningfully by the game code [13/13]
*:fwg6:function (or constructor) overloading demonstrated [13/13]
*:fwg6:working collision detection between game objects [26/26]
*:fwg6:game has scoring (or lives/timer) and a win/lose condition [13/13]
*:fwg6:game uses at least one texture and one sound [13/13]
*:fwg6:source file, XML file, build script, and cartridge submit [13/13]
*:fwg6:code compiles, cartridge builds with no warning or error [13/13]
*:fwg6:committed project related changes to private semester repo [13/13]
*:fwg6:grabit of project data by Sunday prior to duedate [13/13]

Additionally:

  • Solutions not abiding by spirit of project will be subject to a 50% overall deduction
  • Solutions not utilizing descriptive why and how comments will be subject to a 25% overall deduction
  • Solutions not utilizing indentation to promote scope and clarity or otherwise maintaining consistency in code style and presentation will be subject to a 25% overall deduction
  • Solutions not organized and easy to read (assume a terminal at least 90 characters wide, 40 characters tall) are subject to a 25% overall deduction