Wednesday, October 25, 2017

OpenGL Tutorial - Part 3 of 3

Welcome back to my OpenGL tutorial, this time we're on the third and final part where we will get to experience some hands-on coding with some of the more basic features of OpenGL to implement something a little more cooler than a simple stationery white square.

I'm going to dump some code that draws a rotating 3D-cube on the screen in various colors.  The first thing I'd like to do after that is explain what the code does to get the cube to display on the screen.  After that, we can better guide the reader through the process of implementing some extra bits.

At first, this code will draw a rotating 3D-cube, stationery on a black background.  And with some extra effort, we'll be animating the cube so that it bounces around inside a larger defined boundary cube.  This is like the old screen saver with a bouncing circle along the edges of your monitor.  Except this will be a cube.

Starting Point

Copy-and-paste the following code.  This is some code that I wrote.  Run it to make sure that it compiles, builds and executes properly.  It should be a rotating cube.

#include <GL\glew.h>
#include <GL\freeglut.h>
 
//disables console window so only our application window displays
#pragma comment( linker, "/subsystem:\"windows\" /entry:\"mainCRTStartup\"" )
 
//defining variables here
float rot = 0.01;
 
//draws six faces of a cube with varying colors
void drawCube(void)
{
    glBegin(GL_QUADS);
    {
        glColor3f(0.0f, 1.0f, 0.0f);
        glVertex3f(1.0f, 1.0f, -1.0f);
        glVertex3f(-1.0f, 1.0f, -1.0f);
        glVertex3f(-1.0f, 1.0f, 1.0f);
        glVertex3f(1.0f, 1.0f, 1.0f);
 
        glColor3f(1.0f, 0.5f, 0.0f);
        glVertex3f(1.0f, -1.0f, 1.0f);
        glVertex3f(-1.0f, -1.0f, 1.0f);
        glVertex3f(-1.0f, -1.0f, -1.0f);
        glVertex3f(1.0f, -1.0f, -1.0f);
 
        glColor3f(1.0f, 0.0f, 0.0f);
        glVertex3f(1.0f, 1.0f, 1.0f);
        glVertex3f(-1.0f, 1.0f, 1.0f);
        glVertex3f(-1.0f, -1.0f, 1.0f);
        glVertex3f(1.0f, -1.0f, 1.0f);
 
        glColor3f(1.0f, 1.0f, 0.0f);
        glVertex3f(1.0f, -1.0f, -1.0f);
        glVertex3f(-1.0f, -1.0f, -1.0f);
        glVertex3f(-1.0f, 1.0f, -1.0f);
        glVertex3f(1.0f, 1.0f, -1.0f);
 
        glColor3f(0.0f, 0.0f, 1.0f);
        glVertex3f(-1.0f, 1.0f, 1.0f);
        glVertex3f(-1.0f, 1.0f, -1.0f);
        glVertex3f(-1.0f, -1.0f, -1.0f);
        glVertex3f(-1.0f, -1.0f, 1.0f);
 
        glColor3f(1.0f, 0.0f, 1.0f);
        glVertex3f(1.0f, 1.0f, -1.0f);
        glVertex3f(1.0f, 1.0f, 1.0f);
        glVertex3f(1.0f, -1.0f, 1.0f);
        glVertex3f(1.0f, -1.0f, -1.0f);
        glEnd();
    }
}
 
//draw our cube at its location and rotation angle
void render(void)
{
    //clear, reset, recenter, update rotation
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    glLoadIdentity();
 
    //snap our cube to its position and rotate it
    glTranslatef(-1.5f, 0.0f, -15.0f);
    glRotatef(rot, 1, 1, 0);
 
    //draw our cube
    drawCube();
 
    //standard call to dump our drawing into the next frame
    glutSwapBuffers();
}
 
//animation for rotating the cube
void animate()
{
 
    //update rotation angle
    rot += 1;
    rot = rot >= 360 ? 0 : rot;
 
    //refresh screen
    glutPostRedisplay();
}
 
//reshapes the drawing when window is resized
void reshape(int wint h)
{
    glViewport(0, 0, wh);
    glMatrixMode(GL_PROJECTION);
    glLoadIdentity();
    gluPerspective(45.0f, (float)w / (float)h, 1, 1000);
    glMatrixMode(GL_MODELVIEW);
    glLoadIdentity();
}
 
//main entry point
int main(int argcchar** argv)
{
    //standard initialization routine
    glutInit(&argcargv);
    glutInitWindowPosition(100, 100);
    glutInitWindowSize(320, 230);
    glutInitDisplayMode(GLUT_DEPTH | GLUT_DOUBLE | GLUT_RGBA);
    glutCreateWindow("Simple Shape Drawing");
 
    //color to draw background
    glClearColor(0.0, 0.0, 0.0, 0.0);
 
    //enable depth viewing
    glEnable(GL_DEPTH_TEST);
 
    //call our display function which actually does some drawing
    glutDisplayFunc(render);
    glutIdleFunc(animate);
    glutReshapeFunc(reshape);
 
    //enters our display functions into a repeating loop
    glutMainLoop();
 
    return 0;
}

Explaining Code

Let's dig deeper and see what our code is really doing.

Creating the Window

Let's have a look at the main entry point: the method named main.  This serves as a basic naming function for C++ to know where to begin.  Inside that method is a pretty standard OpenGL initialization routine.  The first glutInit call initializes the OpenGL engine with the given command line arguments to main.  Then, the window position and size is initialized just after.  You can modify the values in these if you like.

The glutInitDisplayMode defines the display mode for the created window.  You can play around with these settings if you want.  The use of the "|" (bitwise or) operator allows settings to be chained together so that you can specify multiple different settings in the same line.  If you remove the GLUT_DOUBLE, which stands for double buffering, you may notice the cube rotates very hyper, i.e. very less smoothly.

Finally, glutCreateWindow will form the window with the given title.  You can name this however you want.  Shortly after, glEnable(GL_DEPTH_TEST) allows us to view our renderings in 3D by performing a depth test so that only the frontward faces are visible.  If you remove this line, you'll notice that you can see all six faces of the cube at once as they're blended together in weird ways.

Calling our Drawers

Next, we reference our method where the actual cube drawing takes place with a trio of methods.  The glutDisplayFunc is what gets called when the window first displays or displays after being gone.  The glutIdleFunc gets called constantly.  Lastly, the glutReshapeFunc is what will be called when changing the size of the window when it is running.  Here, we use the display func to store our actual drawing of the cube, and the idle func to animate our cube in its rotational manner.

Lastly, the reshape function is generally used to maintain any aspect ratio of the drawn shapes.  In this demo, we use it to also grow or shrink the cube as the window changes size.  You can see how this is done in the reshape method, which utilizes a bit of a standard routine.  The important call is in gluPerspective, which sets the field of view to a 45 degree cone, and the aspect ratio to whatever the ratio of the window is.  The final two parameters to that method define the clipping points in the depth.  Here, anything outside of Z=1 and Z=1000 will be clipped and not drawn.

Drawing the Cube

As stated just a bit ago, our cube will get drawn in the display func, which we've established is the function named render.  Going there, we notice the first thing done is to clear the screen and load the identity via glLoadIdentity.  This refers to the identity matrix, in which OpenGL uses a matrix to store the contents of drawn shapes.  This must be reset at the beginning of every display func so that it can successfully redraw the intended drawing properly from scratch.

The next thing done is to translate our "position" to the location where we want to draw the cube.  Again, this has to do with the matrix OpenGL uses.  We will first move to the location, and then we will perform the rotation at that point.  When rotation and translation are done in opposite order, the cube will rotate around the point instead of in place at the point.  This may be counter-intuitive, but to me it seems backwards.  So it may be best to think of these operations as working in reverse.

Then, the cube is drawn in our custom method named drawCube.  This is done by telling OpenGL that we're about to begin drawing some "gl_quads" primitives with glBegin(GL_QUADS).  These are four-sized rectangles specified by four vertices.  To draw a cube with rectangles, we basically stitch them together on common vertices to define the six faces.  This part may seem a bit too "numbery", so you may want to suffice it enough to say, that this sequence of vertex assignments is indeed a cube.  However, note at the beginning of each face, we've defined a new color so that our faces are more distinguishable when drawn.  Lastly, a call to glEnd indicates that we're done drawing these primitives.  I like to use curly brackets around these sections, but note that they're more for visual code style and are not necessary.

Finally, the end of our render function calls glutSwapBuffers as part of its double buffering routine to increase the smoothness of the animation.

Animating the Cube

Our last code bit basically is the animate method which is called by our constantly repeating idle func.  This part is simple: all we do is increment the rotation angle and keep it bounded cyclically between 0 and 360.  The important bit is when glutPostRedisplay is called to send a signal so that our display function is called to draw our cube.

That should be it!  Now that we've reviewed pretty much all the basics of our demo program, we can begin to add a feature to it.  Let's try to get this rotating cube to now move and then get it to bounce off walls.  Finally, we'll draw the wall we're bouncing the cube off and have ourselves a bit of a nice screensaver type of program.

Adding Features

Now for the fun part.  Basically, I'd like to get the cube to bounce off the walls like a screensaver, except in 3D.

More Member Variables

I've already provided the variable for our rotation angle, but let's add several more that we'll need in adding our feature.  Place these variables below just below the rotation angle variable, rot, so that these are inside our class file but outside of any methods.

Location, Size, Velocity

First, let's add the location.  This will be a 3D-coordinate system location defined as an array of 3 floating point values.  The first two represent the position left/right (X) and top/down (Y) in the view.  The last value represents the depth (Z).  Since we more or less want the view to be some distance away from us, we specify an initial deep depth of -15.

float location[3] = { -1.5f, 0.0f, -15.0f }; //coordinate vector

To control the size of our cube, let's define a scale variable that shrinks or inflates our cube based on its setting.  A value of 0.5 halves the size, and a value of 2 will double it.  We will see its use later and how it can be used to scale our drawn shapes.

float scale = 0.5;

To animate our cube, we define a 3D-velocity that represents how much our cube moves in every direction.  As far as animation goes, this is the amount the cube will move across every second.

float velocity[3] = { 1.0f, 1.0f, 1.0f }; //velocity vector

Boundary Walls

To allow our cube to bounce properly, we'll need to define where the walls are.  This is a bounding box with six faces, and so hence we'll have six values to define the location of those planar faces.

float velocity[3] = { 1.0f, 1.0f, 1.0f }; //velocity vector
float lbounds[3] = {-5.0f, -5.0f, -50.0f}; //boundary box lower limits

Animation Timing

Since the animation routine will be called as indeterminate intervals, we'll have to handle animation by checking against the amount of time between calls and scaling our velocity with those elapsed time periods.  These below variables will allow us to collect the time since epoch and use the elapsed milliseconds to more smoothly draw our animated cube.

First, add an import with the include tag at the top of the file:

#include <chrono>

Then, add these variables with our others:

__int64 elapsedTimeInMS = 0; //time since last draw
__int64 lastTimeEpoch = std::chrono::duration_cast<std::chrono::milliseconds>(
    std::chrono::system_clock::now().time_since_epoch()).count(); //current time

Updates to Drawing

Since we have added some information to describe the position of our cube, let's update our display function so that the cube is always drawn at its given location.  To do this, simply edit our line where the glTranslatef function is located and pass in our shape information:

glTranslatef(location[0], location[1], location[2]);

As for another update, since we have a variable to store the size or scale of our shape, let's also add that below the call to glRotatef:

glScalef(scale, scale, scale);

The scale call basically scales what is drawn by a factor along each of the three dimensions.  If edited, we could scale unevenly across our three dimensions.  For example, a scale of 0.5 will halve the size, and a scale of 2.0 would double it.

Updating Animation

Now that we have some standard location animation variables, we can get to making our cube move.  Inside our idle function, that is, over in our animate function, we can make a few adjustments.  The first thing we'll want to do is find out how much time has elapsed since our last call to animate.  This allows us to smoothly move the cube at its given velocity instead of moving in large hops.  Add this code to the top of our animate function:

    //get elapsed milliseconds
    __int64 timeNow = std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::system_clock::now().time_since_epoch()).count();
    elapsedTimeInMS = timeNow - lastTimeEpoch;
    lastTimeEpoch = timeNow;

Next, we can make the position updates using a loop across our three dimensions:

    //update position and detect wall bouncing
    for (int i = 0; i < 3; i++)
    {
        location[i] = location[i] + velocity[i] * (1.0/ elapsedTimeInMS);
    }  

Finally, we can then implement velocity changes when a "bounce" off the boundary wall is detected.  To do this, whenever a bounce is detected in that dimension, we simply reverse the velocity along that direction.  Add this code after the position updates, but inside our loop:

        if (location[i] > ubounds[i] || location[i] < lbounds[i])
        {
            velocity[i] *= -1; //when wall bounce detected, just simply go in opposite direction
        }

The last part of the animation should remain the same.  When put all together, it should look like this:

//routine to animate our cube with its velocity and elapsed time since last call
void animate(void)
{   
    //get elapsed milliseconds
    __int64 timeNow = std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::system_clock::now().time_since_epoch()).count();
    elapsedTimeInMS = timeNow - lastTimeEpoch;
    lastTimeEpoch = timeNow;
 
    //update position and detect wall bouncing
    for (int i = 0; i < 3; i++)
    {
        location[i] = location[i] + velocity[i] * (1.0/ elapsedTimeInMS);
        if (location[i] > ubounds[i] || location[i] < lbounds[i])
        {
            velocity[i] *= -1; //when wall bounce detected, just simply go in opposite direction
        }
    }   
 
    //update rotation angle
    rot += 1;
    rot = rot >= 360 ? 0 : rot;
 
    //refresh screen
    glutPostRedisplay();
}

Drawing Our Boundary Walls

If you run the code thus far, you should see our cube bounce about its boundary box.  The trouble is, we can't see where our boundary walls are and that would be a nice feature.  So let's work on that.

What we want to do is draw a wire-frame cube.  To do the drawing, we'll do something similar to what was done for drawing our solid cube.  Let's add a method for that first:

//draw the boundary box where cube bounces as a wireframe
void drawBounds(void)
{
    glColor3f(1.0f, 1.0f, 1.0f);
    glPolygonMode(GL_FRONT_AND_BACKGL_LINE);
    glBegin(GL_QUADS);
    {      
        glVertex3f(ubounds[0], ubounds[1], lbounds[2]);
        glVertex3f(lbounds[0], ubounds[1], lbounds[2]);
        glVertex3f(lbounds[0], ubounds[1], ubounds[2]);
        glVertex3f(ubounds[0], ubounds[1], ubounds[2]);
 
        glVertex3f(ubounds[0], lbounds[1], ubounds[2]);
        glVertex3f(lbounds[0], lbounds[1], ubounds[2]);
        glVertex3f(lbounds[0], lbounds[1], lbounds[2]);
        glVertex3f(ubounds[0], lbounds[1], lbounds[2]);
 
        glVertex3f(ubounds[0], ubounds[1], ubounds[2]);
        glVertex3f(lbounds[0], ubounds[1], ubounds[2]);
        glVertex3f(lbounds[0], lbounds[1], ubounds[2]);
        glVertex3f(ubounds[0], lbounds[1], ubounds[2]);
 
        glVertex3f(ubounds[0], lbounds[1], lbounds[2]);
        glVertex3f(lbounds[0], lbounds[1], lbounds[2]);
        glVertex3f(lbounds[0], ubounds[1], lbounds[2]);
        glVertex3f(ubounds[0], ubounds[1], lbounds[2]);
 
        glVertex3f(lbounds[0], ubounds[1], ubounds[2]);
        glVertex3f(lbounds[0], ubounds[1], lbounds[2]);
        glVertex3f(lbounds[0], lbounds[1], lbounds[2]);
        glVertex3f(lbounds[0], lbounds[1], ubounds[2]);
 
        glVertex3f(ubounds[0], ubounds[1], lbounds[2]);
        glVertex3f(ubounds[0], ubounds[1], ubounds[2]);
        glVertex3f(ubounds[0], lbounds[1], ubounds[2]);
        glVertex3f(ubounds[0], lbounds[1], lbounds[2]);
        glEnd();
    }
    glPolygonMode(GL_FRONT_AND_BACKGL_FILL);
}

The first thing to note is that we define a polygon mode in glPolygonMode(GL_FRONT_AND_BACKGL_LINEto be that of a wireframe, using constants that let us know only draw the lines and not fill to a solid color.  The first constant (GL_FRONT_AND_BACK) tells OpenGL to go ahead and draw all faces, even the ones we can't see.  We unset this at the end so that our cube drawing doesn't draw in the same way.

Finally, we can hook up to our new drawBounds method from our display function.  This part is simple, but the complexity behind it is a little more sophisticated.  After having drawn our little cube, we'll want to undo what we've done to the matrix in our translate, rotate and scale operations.  In this way, we can draw the wireframe boundary box in its actual place, devoid of any rotation, scale or translation.

The trick to undoing those operations is in the math.  To undo a scale, we'd need an inverse.  To undo our translation and rotation, we just need a negative.  Additionally, the order here reflects that in the way they were first performed.  Since our scale was last to perform, it is first to be undone, and so on.

    //undo our snap, rotate and scale
    glScalef(1 / scale, 1 / scale, 1 / scale);
    glRotatef(-rot, 1, 1, 0);
    glTranslatef(-location[0], -location[1], -location[2]);
    

Afterwards, we can now draw our boundary box and complete the method.

    //draw the boundary wall
    drawBounds();

Wrapping Up

That should do it.  We've finished doing what we set out to do.  For reference, here is the completed new code.

#include <GL\glew.h>
#include <GL\freeglut.h>
#include <chrono>
 
//disables console window so only our application window displays
#pragma comment( linker, "/subsystem:\"windows\" /entry:\"mainCRTStartup\"" ) 
 
//defining variables here
float rot = 0.01;
float scale = 0.5;
float location[3] = { -1.5f, 0.0f, -15.0f }; //coordinate vector
float velocity[3] = { 1.0f, 1.0f, 1.0f }; //velocity vector
float lbounds[3] = {-5.0f, -5.0f, -50.0f}; //boundary box lower limits
float ubounds[3] = { 5.0f, 5.0f, -10.0f }; //boundary box upper limits
__int64 elapsedTimeInMS = 0; //time since last draw
__int64 lastTimeEpoch = std::chrono::duration_cast<std::chrono::milliseconds>(
    std::chrono::system_clock::now().time_since_epoch()).count(); //current time
 
//routine to animate our cube with its velocity and elapsed time since last call
void animate(void)
{   
    //get elapsed milliseconds
    __int64 timeNow = std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::system_clock::now().time_since_epoch()).count();
    elapsedTimeInMS = timeNow - lastTimeEpoch;
    lastTimeEpoch = timeNow;
 
    //update position and detect wall bouncing
    for (int i = 0; i < 3; i++)
    {
        location[i] = location[i] + velocity[i] * (1.0/ elapsedTimeInMS);
        if (location[i] > ubounds[i] || location[i] < lbounds[i])
        {
            velocity[i] *= -1; //when wall bounce detected, just simply go in opposite direction
        }
    }   
 
    //update rotation angle
    rot += 1;
    rot = rot >= 360 ? 0 : rot;
 
    //refresh screen
    glutPostRedisplay();
}
 
//draw the boundary box where cube bounces as a wireframe
void drawBounds(void)
{
    glColor3f(1.0f, 1.0f, 1.0f);
    glPolygonMode(GL_FRONT_AND_BACKGL_LINE);
    glBegin(GL_QUADS);
    {      
        glVertex3f(ubounds[0], ubounds[1], lbounds[2]);
        glVertex3f(lbounds[0], ubounds[1], lbounds[2]);
        glVertex3f(lbounds[0], ubounds[1], ubounds[2]);
        glVertex3f(ubounds[0], ubounds[1], ubounds[2]);
 
        glVertex3f(ubounds[0], lbounds[1], ubounds[2]);
        glVertex3f(lbounds[0], lbounds[1], ubounds[2]);
        glVertex3f(lbounds[0], lbounds[1], lbounds[2]);
        glVertex3f(ubounds[0], lbounds[1], lbounds[2]);
 
        glVertex3f(ubounds[0], ubounds[1], ubounds[2]);
        glVertex3f(lbounds[0], ubounds[1], ubounds[2]);
        glVertex3f(lbounds[0], lbounds[1], ubounds[2]);
        glVertex3f(ubounds[0], lbounds[1], ubounds[2]);
 
        glVertex3f(ubounds[0], lbounds[1], lbounds[2]);
        glVertex3f(lbounds[0], lbounds[1], lbounds[2]);
        glVertex3f(lbounds[0], ubounds[1], lbounds[2]);
        glVertex3f(ubounds[0], ubounds[1], lbounds[2]);
 
        glVertex3f(lbounds[0], ubounds[1], ubounds[2]);
        glVertex3f(lbounds[0], ubounds[1], lbounds[2]);
        glVertex3f(lbounds[0], lbounds[1], lbounds[2]);
        glVertex3f(lbounds[0], lbounds[1], ubounds[2]);
 
        glVertex3f(ubounds[0], ubounds[1], lbounds[2]);
        glVertex3f(ubounds[0], ubounds[1], ubounds[2]);
        glVertex3f(ubounds[0], lbounds[1], ubounds[2]);
        glVertex3f(ubounds[0], lbounds[1], lbounds[2]);
        glEnd();
    }
    glPolygonMode(GL_FRONT_AND_BACKGL_FILL);
}
 
//draws six faces of a cube with varying colors
void drawCube(void)
{
    glBegin(GL_QUADS);
    {
        glColor3f(0.0f, 1.0f, 0.0f);
        glVertex3f(1.0f, 1.0f, -1.0f);
        glVertex3f(-1.0f, 1.0f, -1.0f);
        glVertex3f(-1.0f, 1.0f, 1.0f);
        glVertex3f(1.0f, 1.0f, 1.0f);
 
        glColor3f(1.0f, 0.5f, 0.0f);
        glVertex3f(1.0f, -1.0f, 1.0f);
        glVertex3f(-1.0f, -1.0f, 1.0f);
        glVertex3f(-1.0f, -1.0f, -1.0f);
        glVertex3f(1.0f, -1.0f, -1.0f);
 
        glColor3f(1.0f, 0.0f, 0.0f);
        glVertex3f(1.0f, 1.0f, 1.0f);
        glVertex3f(-1.0f, 1.0f, 1.0f);
        glVertex3f(-1.0f, -1.0f, 1.0f);
        glVertex3f(1.0f, -1.0f, 1.0f);
 
        glColor3f(1.0f, 1.0f, 0.0f);
        glVertex3f(1.0f, -1.0f, -1.0f);
        glVertex3f(-1.0f, -1.0f, -1.0f);
        glVertex3f(-1.0f, 1.0f, -1.0f);
        glVertex3f(1.0f, 1.0f, -1.0f);
 
        glColor3f(0.0f, 0.0f, 1.0f);
        glVertex3f(-1.0f, 1.0f, 1.0f);
        glVertex3f(-1.0f, 1.0f, -1.0f);
        glVertex3f(-1.0f, -1.0f, -1.0f);
        glVertex3f(-1.0f, -1.0f, 1.0f);
 
        glColor3f(1.0f, 0.0f, 1.0f);
        glVertex3f(1.0f, 1.0f, -1.0f);
        glVertex3f(1.0f, 1.0f, 1.0f);
        glVertex3f(1.0f, -1.0f, 1.0f);
        glVertex3f(1.0f, -1.0f, -1.0f);
        glEnd();
    }
}
 
//draw our cube at its location and rotation angle
void render(void)
{
    //clear, reset, recenter, update rotation
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    glLoadIdentity();
 
    //snap our cube to its position, rotate it and scale it
    glTranslatef(location[0], location[1], location[2]);
    glRotatef(rot, 1, 1, 0);
    glScalef(scale, scale, scale);
 
    //draw our cube
    drawCube();
    
    //undo our snap, rotate and scale
    glScalef(1 / scale, 1 / scale, 1 / scale);
    glRotatef(-rot, 1, 1, 0);
    glTranslatef(-location[0], -location[1], -location[2]);
    
    //draw the boundary wall
    drawBounds();
 
    //standard call to dump our drawing into the next frame
    glutSwapBuffers();
}
 
//reshapes the drawing when window is resized
void reshape(int wint h)
{
    glViewport(0, 0, wh);
    glMatrixMode(GL_PROJECTION);
    glLoadIdentity();
    gluPerspective(45.0f, (float)w / (float)h, 1, 1000);
    glMatrixMode(GL_MODELVIEW);
    glLoadIdentity();
}
 
//main entry point
int main(int argcchar** argv) {
    //standard initialization routine
    glutInit(&argcargv);
    glutInitWindowPosition(100, 100);
    glutInitWindowSize(320, 230);
    glutInitDisplayMode(GLUT_DEPTH | GLUT_DOUBLE | GLUT_RGBA);
    glutCreateWindow("Simple Shape Drawing");
 
    //color to draw background
    glClearColor(0.0, 0.0, 0.0, 0.0);
 
    //more standard things
    glEnable(GL_DEPTH_TEST);
    glHint(GL_PERSPECTIVE_CORRECTION_HINTGL_NICEST);
 
    //call our display function which actually does some drawing
    glutDisplayFunc(render);
    glutIdleFunc(animate);
    glutReshapeFunc(reshape);
 
    //enters our display functions into a repeating loop
    glutMainLoop();
 
    return 0;
}

Road Map:

Part 1 - Introduction
Part 2 - Setting up OpenGL in Visual Studio
Part 3 - Implementing The Boundary Bouncing Cube

OpenGL Tutorial - Part 2 of 3

Welcome back to my three part OpenGL tutorial.  In the last part, we briefly introduced the idea of OpenGL, but in this part of the tutorial, we will guide the reader through the process of setting up a development environment so that they can write code and run programs in OpenGL.

Without any further ado, here are the steps.

1. Download Glut

The core OpenGL headers and libraries come from glut/freeglut.  Use the link below to download freeglut from the MSVC section (this was listed in a link with text "Download freeglut 3.0.0 for MSVC" as of this writing.)  Go ahead and keep the zip around for later reference.

2. Download Glew

Short for OpenGL Extension Wrangler Library.  This contains a lot of extended features that you may not need until you become more advanced with working in OpenGL.  You can obtain this from the link here:


3. Setup directories and copy files

Set up a directory so that you can copy/move the downloaded files from steps 1 and 2.  This can be mostly anywhere, but you may want to keep it simple.  For example, lets use:
  • Central root location for everything: E:\opengl
  • Place for code and projects to go: E:\opengl\projects
  • Put your library files (i.e. freeglut; other dependencies) in here: E:\opengl\lib
After creating the file structure above, unzip the downloads from steps 1 and 2 and paste their folders into E:\opengl\lib.  So your file tree should look something like:

E:\OPENGL
├───lib
│   ├───freeglut-3.0.0
│   └───glew-2.1.0
└───projects

Note that you can output cool text trees like this using the tree command in a terminal window on both Windows and Unix systems.

4. Create C++ Project

In this tutorial, we're going to create a simple OpenGL project to demonstrate some of its 3D capabilities.  This demo will just be a cube that bounces around inside of a 3D box, utilizing some simple math of calculating angles of the bounce off each wall, as well as the simple physics of constant animating velocity and rotation.

Fire up Visual Studio (in this guide, version 2015 is used) and create a new project through the dialog in File > New > Project.  In here, search for Visual C++ in the templates section on the left, and then select Empty Project from the middle.  Give the project a name, like DemoOpenGL, and also click on Browse to place the project in your projects directory from the previous step.

Now your file structure should appear similar to this:

E:\OPENGL
├───lib
│   ├───freeglut-3.0.0
│   └───glew-2.1.0
└───projects
    └───DemoOpenGL

4.1. Add First Source

First, we'll need to add at least one source file.  Right click on the DemoOpenGL project node in the Solution Explorer and navigate to Add > New Item.  Find Visual++ from the left, and select C++ file in the middle.  Name the file main.cpp and click the Add button.

Doing this enables some project settings for the next step so that we can configure properly.


5. Configuring your project

This step is an important one for anyone developing in OpenGL.  It is probably the most complicated one, and so its important to understand what's going on and become comfortable creating new projects with ease.

There's three things we basically need:

1. To see the OpenGL "include" header files.
2. To resolve external references via linked libraries
3. Runtime dlls

While you're doing this, be aware that there are two configurations: 32-bit and a 64-bit.  While headers are platform-independent, the dll's and libs are not, and hence, we will provide forked instructions for both platforms.

To make sure your build configurations have both a 32-bit and 64-bit setting, check out the box just to the right of where it says "Debug" on Visual Studio (up at the top, left of the middle).  Currently, it should by default say x86.  If you already have a setting in there for x64, then you should be fine.  The x86 is a 32-bit version while the x64 is the 64-bit version.  If not, then you can go inside to create a new build configuration (this should be simple).

5.1. Including headers

Inside Visual Studio, get to the project properties.  To do this, right click on your DemoOpenGL project node in the Solution Explorer and navigate to Properties.  Make sure the Configuration drop-down at the top-left is selected to "All Configurations".

Headers are platform independent, so be sure to set these settings under "All Platforms" in the drop down at the top-middle.

Navigate to C/C++ > General.  On the right, look for Additional Include Directories.  Click in the box beside it, and there should be a little down-arrow at the far right.  Click on that and then click on <Edit...>.  This brings up a dialog window where you can specify the location of directories to "include" so that C++ knows where to look in its attempts to resolve external references.

In that new little window that popped up, click on the New Line icon (looks like a folder).  This inserts a new line in the text area and you can click on the "..." button to browse to a location to include.  We want to do this twice and point to the include folders of each of our libraries:

 - e:\opengl\lib\freeglut-3.0.0\include
 - e:\opengl\lib\glew-2.1.0\include

Make sure you did this for "Additional Include Directories" and not "Additional #using Directories".

Click OK.


5.2. Including libraries

Next, we'll be doing something similar around libs instead of headers.  Inside the project properties dialog, browse to Configuration Properties > Linker > General and look for Additional Library Directories on the right.  Before you click, check that the Platform drop-down box is selected on the correct platform (Win32 is 32-bit and x64 is 64-bit).

With the correct platform, click the text box and then on the little arrow and then on <Edit...>.  Another similar window from the last step will launch.  Click the New Line icon and on the "..." button to browse to add the following folders:

[64 bit Version]
 - e:\opengl\lib\freeglut-3.0.0\lib\x64
 - e:\opengl\lib\glew-2.1.0\lib\Release\x64

[32 bit Version]
 - e:\opengl\lib\freeglut-3.0.0\lib
 - e:\opengl\lib\glew-2.1.0\lib\Release\Win32

Click Apply.

One more thing needs to be done in this section.  The above specified where libs could be found, but now we need to specify what libs to grab.  This part is platform independent, so change the Platform selector to All Platforms.

Go to Configuration Properties > Linker > Input and click in Additional Dependencies.  Bring up the dialog box with the <Edit...> link and type into the top empty text box the following libs:

freeglut.lib  (and press enter to get a new line)
glew32.lib

Click OK, and then OK again from the Property Pages.

5.3. Copy dll files

Last but not least, lets move our dll files over into our bin output folder.  This is also platform-dependent, so be sure to copy the correct files.

[64 bit]
Copy:
 - e:\opengl\lib\freeglut-3.0.0\bin\x64\freeglut.dll
 - e:\opengl\lib\glew-2.1.0\bin\Release\x64\glew32.dll (not a typo)

Paste into:
 - e:\opengl\projects\DemoOpenGL\x64\Debug

[32 bit]
Copy:
 - e:\opengl\lib\freeglut-3.0.0\bin\freeglut.dll
 - e:\opengl\lib\glew-2.1.0\bin\Release\Win32\glew32.dll

Paste into:
 - e:\opengl\projects\DemoOpenGL\Debug

6. Write code

Go ahead and paste this code into main.cpp, which I got from https://www.badprog.com/c-opengl-hello-world.  It should display a simple white square when run in the next step.

#include <GL/glut.h>
 
void displayMe(void)
{
    glClear(GL_COLOR_BUFFER_BIT);
    glBegin(GL_POLYGON);
    glVertex3f(0.0, 0.0, 0.0);
    glVertex3f(0.5, 0.0, 0.0);
    glVertex3f(0.5, 0.5, 0.0);
    glVertex3f(0.0, 0.5, 0.0);
    glEnd();
    glFlush();
}
 
int main(int argc, char** argv)
{
    glutInit(&argc, argv);
    glutInitDisplayMode(GLUT_SINGLE);
    glutInitWindowSize(300, 300);
    glutInitWindowPosition(100, 100);
    glutCreateWindow("Hello world :D");
    glutDisplayFunc(displayMe);
    glutMainLoop();
    return 0;
}


7. Compile/build/run

There shouldn't be any red-lined errors.  If there are red-underlined errors indicating that Visual Studio is unable to find a header file, then you may want to revisit step 5.1 to ensure the directories were set properly.

If there are linker errors, you may want to revisit step 5.2, which indicates that the libraries can't be found to resolve those references.

Lastly, if there is a problem running the application but not in building, you may get an error specifying that a required dll was not found.  For this, revisit step 5.3

When the above code is run, it should display a stationary white square on a black background.  Pretty simplistic "hello world" program of sorts.  In the next step, we will work around something a little less basic to give the reader a little more hands-on experience.

Road Map:

Part 1 - Introduction
Part 2 - Setting up OpenGL in Visual Studio
Part 3 - Implementing The Boundary Bouncing Cube

OpenGL Tutorial - Part 1 of 3




Overview


Welcome to my tutorial on OpenGL, a graphics package for C++ that supports rendering of views in 3D and also in 2D.  It's a nice library for developing games as well as simulations that require visual representation.  Check out its website for a lot more information: https://www.opengl.org/about/.

This tutorial focuses on getting a developer up and started with using OpenGL using a Visual Studio environment.  This writing assumes a Windows platform of either 32-bit or 64-bit, but OpenGL is supported on many others, including Linux and Mac.

In the first several steps, we'll introduce the developer to setting up OpenGL for the first time, and then we will write a short program to demonstrate some of its 3D capabilities.

I learned about OpenGL while taking a trio of graduate level computer science courses at West Virginia University, the last of which used OpenGL around the topics of medical image analysis.  Some of what I thought the coolest features were focus on surface details such as textures, reflections and shaders, in which pixels and fragments can be dynamically colored based on specific lighting and material conditions.

For this blog, this will be a three-part post.  This first post just serves as an introduction, but the second one will help a user setup a Visual Studio environment where they can begin to work with and write code in OpenGL.  In the third part, I'll introduce some code around a simple little demo so that the user can get their hands on actually changing some given code and implementing the desired behavior.

Road Map:

Part 1 - Introduction
Part 2 - Setting up OpenGL in Visual Studio
Part 3 - Implementing The Boundary Bouncing Cube

Monday, August 21, 2017

What is playability?

What is playability?

When a gamer first takes to the controllers or keyboard to interface themselves with the game, they are creating a seamless connection with themselves and the game.  This interface should be fluid and easy to learn, the same way a person has a intimate connection when sitting in the driver seat of the car.  The player shouldn't have to look down to look for buttons, and there should be no distractions, from the game or elsewhere.

When a seamless interface is created, it is said that immersion has occurred.  At this point, the player is in the game and does not realize that they are connected via a controller.  This creates an optimal play experience and it is expected of any gaming experience.  Hence, the act of getting into this "magic circle" called immersion is what playability is all about.

There are a number of things that can distract.  Some are external and the game developer would have no natural control over there.  This could include the door bell ringing, a dog barking, or something as simple as an alarm going off indicating its time to quit playing.

External Distractions

  •  Unintentional
    • Door bell ringing, dog barking, health reasons, etc.
  •  Intentional
    • Scheduled alarms, conscious thoughts, etc.

On the other hand, there are internal sources of distraction that should and must be minimized by the game developer.  These include glitches in the game, discontinuities in stories, poor system performance, or poor network lag (which could be both a problem at the network side or with the poor usage of data streams being sent across the network which could've been made more efficient.)

Internal Distractions

  • Performance
    • Network Latency
    • Inefficient operations, menu takes too long to load, etc
  • Discontinuities
    • Gaps in story, missing but expected considerations or implementations, etc.
  • Defects
    • Glitches that make you go "what the... "
    • Designs that are disconcerting, tutorials that take too long/too obvious, slow load times that aren't due to inefficiencies, etc
Software Performance Engineering
https://www.slideshare.net/TanzaIratier/joe-krall-presentation
Study on latency in games leads to lower overall enjoyability index (http://dl.acm.org/citation.cfm?id=566500.566511&preflayout=tabs)

https://link.springer.com/content/pdf/10.1007/978-3-540-74873-1_53.pdf

Friday, August 18, 2017

The gameplay lifecycle

All games go through this gameplay lifecycle.  Consider the perspective of the gamer.  As an individual, their attention and interest must first be obtained and piqued.  Then, with enough interest and motivation, they will buy the game and begin playing it.  The first play is critical, because it will define how long they play it thereafter.  Eventually after completing or beating the game, they'll put it down, and perhaps do one of two things: either sell it, or keep it for later.  In the case of the latter, they'll wait until the game becomes interesting enough to once again replay it.


Once a game product is finished, and even before, it begins in the Advertising & Marketing stage, where players are given First Glance opportunities.  These could actually come from trailers and other pre-marketing strategies, such as demos or screenshots.  During this time, a player is contemplating whether he or she wants to buy the game when it is available.  The player also develops expectation levels about how good they think the game will be.  If the expectation is more than what is essentially their threshold for buying the game, then they will do so.

At that point, the game is bought, and the player enters the First Play stage.  This represents the game's Playability phase.  At this point, all expectation is either brought to realization or it isn't.  If it is, and the game successfully minimizes distraction from immersion, then the player will keep playing it, absorbing the content and entering the "Game Replay" stage.

At the point of Game Replay, the player has made the conscious choice to play the game for various reason encompassing the aspects of replayability.  All the while, playability considerations remain important, because unplayable content will add to the player's growing list of reasons to quit the game.  If that list of reasons becomes too heavy, the player will quit, which is the final stage of gameplay lifecycles.

Quitting a game isn't so bad as it sounds, as long as its for the right reasons.  The wrong reasons mean that the game didn't provide enough reasons to keep playing, i.e. the aspects of replayability.  Other wrong reasons could mean the game didn't minimize distractions from immersion.  Sometimes such distractions are impossible to avoid -- the player could simply be too busy for good life reasons.  Other distractions should and must be avoided, such as game lag (network or system), discontinuities in the story, bugs and defects, unfair content (i.e. challenge flow zone not correct), and more.

In any case, the player must quit at some point.  This is inevitable.  When that does happen, the player can resume game play at any time at a later date.  If they sell the game, they may be reconvinced through First Glance to buy it again.  If they keep the game, they may decide to simply play it later, with different experiences (playing for impact) or for other reasons.  And lastly, the most common, they quit because they're simply done (or ran out of time) for the day.  Their gameplay session done, they'll go to sleep, wake up the next morning, go to work, come home and have another gameplay session in the Game Replay stage, and so on.

Understanding the gameplay lifecycle is important because, it can help game developers understand how their end-users are behaving.  It is likely something they already know, deep-down inside, but to put it to voice and reason and on paper can be very helpful.  First and foremost, developers should know that every product they make has the destiny of being quit by their end-users.  But that isn't a bad thing as long as its for the right reasons.  Understanding the branch points when players make their decisions can help identify what the reasons might be.  For example, if players aren't committing to purchasing the game, then its because of poor First Glances, which could be due to bad advertising & marketing.  With this kind of information, the developers can then address those concerns, and make their game (or the next one) a little better.

Thursday, August 17, 2017

Advertising and Marketing in the Game Industry

Advertising and marketing are some lesser talked about topics in game design, but I find them important.  They may fall outside the domain of software developers and game designers, but at least a solid theory of what they are and why they're important should be a requisite for any successful game developer.  These topics not only encompass game development, but also other forms of entertainment, such as movie production and book authoring.

Advertising

Advertising is necessary because it lets people know about your game.  It is a complement to virality, and you need both.  With virality, all the advertising in the world will do little good, as virality can work both ways - it can advocate your game or it can alienate your game.  Players who love the game will tell their friends.  Players who don't will also tell their friends that they don't, which will discourage others from buying it, despite seeing it being advertised.

With a high virality, meaning players greatly advocate to their friends that they enjoy the game and that it's worth their time and cost to buy, the game advertises itself locally as popularity spreads.  Without advertisement, that virality will remain localized and will not spread as much globally.  With advertisement in addition to great virality, many "initial contact points" occur, and from within each, the game is spread virally.  This is optimal.

Marketing

Marketing is also just as important.  If you have both advertisement and virality, the popularity does little good if buyers can't find a location to purchase the game.  The internet today is an excellent resource for marketing today, and there are still in-person options available for shops like Gamestop, Walmart, Best Buy and others.  With in-person shops, research must be done to determine how many copies should be provided to the shop, and the rate of purchase for restock.  An audience must also be researched.  The game may sell better in Pennsylvania than it does in Nevada, for example.  If that were the case, Gamestops in Pennsylvania should get more copies of the game to sell.

Marketing via the internet is another topic.  Online retailers like Amazon are already quite popular.  Other retailers need to be advertised so that potential buyers are aware of where to go.  And hence, to market properly sometimes you need to advertise your marketing strategy.

Summary

Both advertisement and marketing is necessary for successful game development.  This also includes virality of the game product, which arises as a result of good game design.  Advertising lets players know about your game, and marketing lets players know how to find it.  Successful marketing thereby also needs to be advertised along with your game.

Tuesday, July 25, 2017

BSP Dungeon Generation

Overview

One topic I love to talk about is an algorithm for building random dungeons.  A dungeon is essentially a maze which could be one of many styles.  The style of dungeons that I enjoy is one where there are connected rooms, and one must crawl their way through the dungeon in search of something, perhaps an exit.

The Binary Space Partitioning (BSP) algorithm is a very interesting one, because it provides an absolutely wonderful way of building a random dungeon in the style that I enjoy.  When combined with dungeon generation, it can develop truly fantastic procedural content generation (PCG).  Behold:

A random dungeon built using the BSP algorithm.  The green triangle is the player and the red stairs represent an exit.
This kind of dungeon is special because it holds a number of properties.  It will always be fully connected, meaning there will never be an area of the dungeon inaccessible from the rest.  Additionally, there will never be any cycles (in terms of rooms), meaning the pathway from any room in the dungeon to any other is unique.

The fully connected property is very cool in PCG.  It prevents the need to perform content validation, meaning the complexity for this kind of dungeon generation is efficient, whereas this is sometimes a drawback to deploying PCG solutions in gaming.

Details

As a recursive algorithm, this kind of dungeon generation is difficult to analyze.  It works by roughly splitting the area until the rooms are small enough, but it also does so in a random manner.  Consider the starting point, which is a dungeon with no interior walls and only an exterior outside boundary wall:

An empty dungeon with only the player and an exit.


The first step of our algorithm is to then choose randomly to either split this in half vertically or horizontally.  Running this with a fixed random seed, I can pause the algorithm and tell it when to stop, allowing me to generate these pictures.  We can see that in the first division, the algorithm chose to split horizontally.

A single partition has generated a dungeon having two rooms.

In choosing to split horizontally, the algorithm chose a semi-random location to do so; somewhere near the middle, for aesthetic purposes, and not riding along the edges of the outer dungeon.  After adding the wall for the split, the next step was to provide an entryway to reach from one side to the other.  This is what allows for the fully connected property.

Going one step further, the algorithm will traverse into both of the newly created halves.

An addition split in the northern room now produces a three-room dungeon.
Here, recursion stepped into the top half, where it again chose to split horizontally.  An entryway was placed, and two more partitions were created.

Four-room dungeon with yet another partition.


Continuing into the top-most of the new partitions, a split was chosen vertically.  The entryway was placed, and recursion continues into both of the new partitions.  Only this time, since the (top left) room was considered sufficiently small enough (termination criteria of the recursion), it stopped subdividing and moves onto the next sibling (top right room).

The same situation happens in that room as well, and so the traversal visits the next sibling, which is the middle room.  The middle is fortunately wide enough to split vertically, although not tall enough to split horizontally, so the choice is forced to be a vertical split.

Five-room dungeon.

And so on.  These middle rooms are small enough.  Hence, we move onto the bottom room which again can only be split vertically, giving us our final result as that split produces sufficiently small rooms.

In each of these partitions, care is given not to select a split that might potentially obscure a previously installed entryway.

The BSP algorithm also has a by-product, called the BSP-Tree, which is easy to see.  The root node is the empty room, and a single split divides the tree into two.  It is always a binary tree and it is with this tree we can prove that the dungeon is fully connected.

Conclusion


There are many applications of the BSP algorithm, but creating dungeons with it is one of my favorite.  Hopefully this blog post will have enlightened a game developer and inspired ideas.  In the future, I will drop some code onto this post and detail how it works to implement this kind of random dungeon generation.


Friday, June 9, 2017

Unit Testing and Test Driven Development (TDD)

Since my time with GE, I've learned a lot about the Agile Software Development process.  It had always been something I was exposed with, especially throughout my doctoral studies.  I mean after all, I did build a medium sized simulation of agile requirements engineering called POM3.  So going forward, I was excited to dig around into the wealth of knowledge already out there surrounding the topic of unit testing.  And here's what I found out.

Overview


Unit tests are an essential part of test driven development (TDD) and is part of the Agile Paradigm for software development.  "Get something working now and perfect it later."  Unit tests provide the most basic form of testing available to the developer and they are a great boon in aiding the developer in implementing features and user stories.  Some benefits are listed here:
  • Very cheap to run given their unitary nature in a decreased complexity environment
  • Offers an entry point to otherwise complex code
  • Very easily facilitates automation as a form of code-validation upon code check-in
  • Can be very entertaining to see failing tests made passing
It can be difficult to convince project managers to the benefit of unit testing.  Why waste time writing unit tests?  The simple answer: it is an effective means to retire risk early, thus saving on project costs in the long run.  It is important to note though, that this kind of investment is not for everyone.  Unit tests require maintenance, and if requirements often change, then unit tests need to be updated.  There are ways to mitigate these hindrances, however, by writing good unit tests that usually lead to good design.

Reading

Kent Beck is one of the premier authors on test driven development and unit testing, having introduced the concepts in 1970.  Your first stop for unit test literature should be here, and its not even that large of a book:
Another great book on Unit Testing, and in .NET, by author Roy Osherove, which is a bit newer:
Martin Fowler is also one of the original authors of the Agile Manifesto.  You can also find a wealth of content on his personal website.  Also a co-author (along with Kent Beck, listed above) of a good read on Refactoring.

Summary: How To Unit Test

TDD proposes unit testing as an integral part of development.  Before even typing a single key stroke, you should sit down and think about the user story you're working on.  That's step one -- to create a list of tests that you want to run.  The list of tests should be driven to cover as many code paths as possible around the user story.  The list should also be sorted in easiest-first order, where easy is defined as requiring the least amount of code to make it pass.
Each unit test should basically contain a standard arrange-act-assert (AAA) framework.  The arrange section provides some setup code that enables the calling of tested functionality in the 'act' portion of the test, and finally, the 'assert' section validates the functionality.  Consider writing each unit test in a reverse fashion: write assertions first, then act, and then arrange.
Writing your first test should occur before writing any production code.  At first, it should be a test that doesn't compile properly.  If you haven't written anything on the production side, then you'll have to provide the basic skeleton necessary to get the test to compile.  When the test is working properly, you'll get your first failing test, and will need to make it green by adding the code necessary to pass the test.  With a passing test comes the time to do any possible refactoring to clean up the code on both the test and production code.  Any then you move on to the next test in the list, repeating this red-green-refactor flow.  As you develop more tests, remember that each previous one must also pass, and the opportunity to refactor will likely be greater.  Typically this process is known as "red-green-refactor".
To Recap:
  1. Come up with a list of tests
  2. Write a failing test
  3. Make changes to code to pass that test
  4. Refactor code and move onto next test in list

Principles

The following are a list of seven ideas that can help with making good unit tests.  This list of principles doesn't necessarily come from any single source – it is a list of principles that came together as a result of digging through dozens of them.  At the end of this page is a rather large list of links that point to all sorts of discussions on unit testing and TDD in general, and they are definitely a recommended read for anyone interested.

#1: Unit Tests Are Not About Finding Bugs

Unit tests are actually about preventing bugs.  This is most often emphasized by automating the tests prior to check-in of new code.  If any existing unit tests are failing, then the build server will not accept your code changes.  This is a preventative measure.

#2: Use Mocks and Don't Use Mocks

You should do both.  Tests should both validate the functionality of code as well as its behavior.  When not mocked, a method can be tested to see if it returns the correct result.  When mocked, the method is tested to see if it was called properly and with the correct parameters. Granted, in smaller projects, mocks may not make a lot of sense given the triviality of the project.  Equally so, larger projects may inhibit the use of not mocking, since mocked objects can help to reduce complexity and provide that necessary level of isolation in a unit test.

#3. Everything Public Must Be Tested

So don't make everything public!  This may be a tendency, but a good design will probably hide a lot of layers of private abstraction behind a public interface.  If you do that, then you only need to test the public interface.  A test-first design will probably facilitate this.  Remember that if a five public classes each have five public dependencies, then you need to test twenty-five things.  And if requirements change, the code changes and all of your unit tests also have to be updated.  That's a lot of work!  

#4: Use Less dependencies

Dependencies should also be tested.  So it makes sense that if you have a lot of them, that's a lot of work.  If requirements change, code must be changed and so too do all the tests.  A lot of dependencies may sometimes exist because of duplication, and a good unit testing practice is to refactor these under common public interfaces.

#5: Don't Mock Everything

To mock a class object, it must be public.  Recalling from principle #3, that could be bad if you mock everything.  There are good suggestions on when to mock.  File IO is one of these, but DB access is probably not.  One good rule of thumb is to mock across significant architectural boundaries, and not within.  Also, don't mock third party tools that are not owned, since they can change at whim.  Remember the purpose of mocks: isolation.

#6: One Behavior One Test

Do try to keep each unit test down to one single assert.  That may not always be possible, but the premise remains.  A "unit" test should be testing a single "unit" of your software.  That *usually* means that one project is one test project, one class is one test class, and one method is at least one test case.  If you have more than one asserts in a single test case, consider the possibility of splitting the test into two cases.

#7: DAMP vs DRY

In principle, favor DRY in production code and DAMP in test code.  DAMP stands for "descriptive and meaningful phrases".  DRY stands for "don't repeat yourself".  Probably coined by Jay Fields (https://leanpub.com/wewut).  These two are not necessarily opposites, and you should quite certainly always be removing duplication as a means to refactor in both production and test code.  More than being a drab sequence of statements in test code, there should be a higher emphasis on "easier to read" in test code.  This means you can escape some standard patterns of good production code design, such as having method names like "TestThatWhenAdding2And2YouGet4" and "SetupTheCalculatorSoThatICanControlItWithMyPrivateAPI".  The reason for this is that whenever a test case fails, it should be immediately and directly clear why it failed.