home download get support documentation weaver games

How to Program a Spacewar Game for Linux using Weaver

Despite being one of the earliest computer game created, the original Spacewar! was a very sophisticated game. It implemented some complex phisical laws to simulate a battle involving two spaceships. It was a multiplayer game where two armed spaceships attempted to shoot one another while maneuvering in the gravity well of a star.

If you already know how to program a Pong Game in Weaver and wish to create something more complex, this is the right place.

Part 1: Planning the Game

All the action should happen in a square area in the center of screen. It should have an area of 800x800 pixels. Inside this area, the players will see a giant star in the certer of screen and two spaceships struggling to attack each other and survive in the gravity well created by the star.

In the left and right side of the screen, the players will see each spaceship's fuel tank. Each time a spaceship use it's propulsion, it spends some fuel. If a spaceship goes to hyperspace (teleport), it also spends some fuel.

Each ship have 5 options of movement: turn right, turn left, fire, use propulsion and go to hyperspace (teleport).

The gravity well and the propulsion should obey the same phisical laws from our universe. The bullets fired by the spaceships shall be affected by the gravity well (contrary to the original Spacewar! game). The dust expelled when a spaceship uses the propulsion also shall be affected by the gravity well.

It's impossible to move outside the screen. Anything that goes too far to the right will reappear again in the left area of the screen. The same happens when something goes too far in any direction.

When a spaceship goes to hyperspace, it disappears for 1 second. After this, it appears again in some random area in the screen. If the player is unlucky, it'll appear again inside the star and then it's game over.

The game should have music and sound effects.

Well, that's a lot of work to do. Let's start!

Part 2: Creating a New Project

Now that we know what we should do, open a terminal and type:

weaver spacewar

Assuming there's no file called "spacewar" in the current directory, after this command, a new directory will be created.

Open with a text editor the file src/game.c. Before the main loop, put the function:

  hide_cursor();

Next, find a line where is written something like if(keyboard[ANY]){ and change it this way:

    get_input();                                                                
    if(keyboard[ESC]){                                                          
      break;                                                                   
    }

Now the game will exit only when we press ESC, not other keys. And we won't have an annoying cursor in the screen.

Part 3: Creating the Star

In most mithologies, the sun and the stars were created before the people. According with the science, this is right, the stars are much more ancient than people and than spaceships. So, in our game we'll create the stars before anything else.

First take these five pictures and save them in the images directory inside the spacewar directory:

star_tiny.png star_small.png star_medium.png star_big.png star_huge.png

The idea is create an animated background with the star in the center growing and shrinking randomly.

Now let's write the code. Inside the spacewar directory, type in the terminal:

weaver star

Then, edit the src/star.h to declare the following functions and macros:

#define STAR_TIME 100000ul
#define STAR_GRAV 700000.0

void initialize_star(void);
void update_star(void);
void draw_star(void);
void destroy_star(void);

struct{
  surface *img[5];
  int state;
  int size;
  struct timeval time;
} star;

All those functions shall be defined in src/star.c. The function initialize_star is responsible to initialize that struct responsible for the data necessary to create our animated star in the background:

// Mallocs all the data needed by the star and also draw the first
// star image in the screen
void initialize_star(void){
  star.state = 0;
  star.size = 26.0;
  // Loading all the sprites for our animated background
  star.img[0] = new_image("star_tiny.png");
  star.img[1] = new_image("star_small.png");
  star.img[2] = new_image("star_medium.png");
  star.img[3] = new_image("star_big.png");
  star.img[4] = new_image("star_huge.png");
  // Drawing the first image
  draw_star();
  // When the star was created:
  gettimeofday(&(star.time), NULL);
}

We are using gettimeofday() because we'll always store in star.time the last time we draw the star in the screen. This way we can check when we'll need to update the background again.

In the last function, we made a call to draw_star(). So, we should define this function too:

// Draws the starfield in the screen
void draw_star(void){
  draw_surface(star.img[star.state], background, 
	       (window_width - star.img[star.state] -> width) / 2,
	       (window_height - star.img[star.state] -> height) / 2);
  draw_rectangle((window_width - star.img[star.state] -> width) / 2 - 1,
		 (window_height - star.img[star.state] -> height) / 2 - 1,
		 star.img[star.state] -> width + 1, 
		 star.img[star.state] -> height + 1, YELLOW);
  blit_surface(background, window, 
	       (window_width - star.img[star.state] -> width) / 2,
	       (window_height - star.img[star.state] -> height) / 2, 
	       star.img[star.state] -> width, star.img[star.state] -> height,
	       (window_width - star.img[star.state] -> width) / 2,
	       (window_height - star.img[star.state] -> height) / 2);
}

The last function basically updates our background surface and then draws the background in the screen. It knows what image to draw checking the star.state. It'll always be called when we update our animated background. This part is handled by the function:

// Updates the star state, making it grows or shrink
void update_star(void){
  struct timeval now;
  unsigned long elapsed_microseconds;
  gettimeofday(&now, NULL);
  elapsed_microseconds = (now.tv_usec - star.time.tv_usec) + 
    1000000 * (now.tv_sec - star.time.tv_sec);
  if(elapsed_microseconds > STAR_TIME){
    gettimeofday(&(star.time), NULL);
    switch(star.state){
    case 0:
      star.state ++;
      star.size += 3.0;
      break;
    case 4:
      star.state--;
      star.size -= 3.0;
      break;
    default:
      if(rand()%2){
	star.state++; 
	star.size += 3.0;
      }
      else{
	star.state--; 
	star.size -= 3.0;
      }
      break;
    }
    draw_star();
  }
}

Notice that when the star isn't in the smallest or biggest size possible, it can grows or shrinks randomly. And that the star changes it's state once each 0.3 seconds.

Now the last function related with stars. When the game is over, we shall call it to free all the memory used by our animated background:

// Free all the memory allocated by the star
void destroy_star(void){
  int i;
  for(i=0; i < 5; i ++)
    destroy_surface(star.img[i]);
}

In game.c, call initialize_star() right before the main loop. Call update_star() inside the main loop, right after the input check. And call destroy_star() right after the main loop. If everything was done correctly, you shall have a project like this: spacewar-0.tar.bz2 (1.5MB).

Enjoy your beautiful star compiling and running the game:

make
./spacewar

Part 4: Building Two Spaceships

Our spaceships will need to rotate freely in the screen. Because this, we'll create them using polygons instead of sprites. It's easier and faster to rotate polygons than sprites.

First let's create a camera to film all the action. Inside src/game.c, before the main loop, declare a new camera:

  camera *cam = new_camera(0.0, 0.0, 800.0, 800.0);
  limit_camera(cam, (window_width - 800)/2, (window_height - 800)/2,
	       800, 800);

That piece of code creates a camera that films a rectangular vector space starting at (0.0, 0.0) and ends at (800.0, 800.0). The second line limits the camera to project the images only on a 800x800 area centered on the screen, not in the whole screen.

Now, inside the spacewar directory, in the terminal, type:

weaver ships

Now inside src/ships.h, declare:

#define DEAD       0
#define ALIVE      1
#define HYPERSPACE 2

#define FUEL       1650.0 // How much fuel each ship have
#define PROPULSORS 2  // Maximum number of propulsors in a ship
#define NUMBER_OF_SHIPS 2
#define SHIP_TURN (MPI / 2) // Angular speed per second
#define SHIP_ACCEL  50.0 //Ship acceleration

struct{
  polygon **body; // Ship's polygons
  unsigned color;
  float x, y;     // Position of the gravity center
  float dx, dy;   // slope
  int status;     // ALIVE, DEAD or HYPERSPACE
  struct timeval time; // Last time the status changed
  float fuel;
  polygon *front;
  polygon *propulsor[PROPULSORS];
} ship[NUMBER_OF_SHIPS];

void initialize_ships(int);
void film_ships(camera *);
void erase_ships(camera *);
void destroy_ships(void);

We'll not use now all the information presented inside the ship struct. But in the future, all them will be useful. The "front", for example, will be important to determine to wich direction the ship shall go when the pilot uses propulsion. The "propulsor" array stores wich polygons are the propulsors and will be used by some visual effects.

The function initialize_ships() initializes the number of ships passed as argument. Initialize a ship means fill it's variables with initial values and create it's polygons. Write it's code in src/ships.c:

// Initializes the number of ships passed as argument.
void initialize_ships(int number){
  if(number < NUMBER_OF_SHIPS){
    int i;
    for(i = NUMBER_OF_SHIPS - 1; i > number - 1; i --)
      ship[i].body =  NULL;
  }
  if(number < 1)
    return;
  // The first ship have 3 polygons:
  ship[0].body = (polygon **) malloc(4*sizeof(polygon *));
  ship[0].body[0] = new_polygon(4, 
				130.0, 159.0,
				125.0, 164.0, // The fuel tank
				119.0, 159.0,
				125.0, 153.0);
  ship[0].body[1] = new_polygon(7,
				133.0, 145.0,
				150.0, 184.0,
				111.0, 167.0,
				122.0, 162.0, // The ship body
				125.0, 164.0,
				130.0, 159.0,
				128.0, 156.0);
  ship[0].body[2] = new_polygon(2,
				144.0, 173.0, // The cockpit
				139.0, 178.0);
  ship[0].body[3] = NULL; // The array must end with a NULL pointer. 
  ship[0].propulsor[0] = ship[0].body[0];
  ship[0].propulsor[1] = NULL; // This ship has only 1 propulsor
  ship[0].front = ship[0].body[1] -> next;
  ship[0].color = BLUE;
  ship[0].x = 136.0; ship[0].y = 170.0;
  ship[0].dx = ship[0].dy = 0.0;
  ship[0].status = ALIVE;
  gettimeofday(&(ship[0].time), NULL);
  ship[0].fuel = FUEL;
  if(number < 2)
    return;
  // The second ship have 7 polygons
  ship[1].body = (polygon **) malloc(8*sizeof(polygon *));
  ship[1].body[0] = new_polygon(2,
				650.0, 614.0, // A ship division
				657.0, 620.0);
  ship[1].body[1] = new_polygon(7,
				649.0, 628.0,
				664.0, 613.0,
				665.0, 616.0,
				666.0, 626.0, // The ship body
				675.0, 638.0,
				662.0, 630.0,
				652.0, 629.0);
  ship[1].body[2] = new_polygon(3,
				649.0, 628.0,
				650.0, 614.0, // The cockpit
				664.0, 613.0);
  ship[1].body[3] = new_polygon(2,
				668.0, 636.0, // Left wing
				668.0, 645.0);
  ship[1].body[4] = new_polygon(2,
				673.0, 632.0, // Right wing
				681.0, 632.0);
  ship[1].body[5] = new_polygon(2,
				674.0, 625.0, // Right propulsor
				691.0, 642.0);
  ship[1].body[6] = new_polygon(2,
				662.0, 638.0,  // Left propulsor
				678.0, 654.0);
  ship[1].body[7] = NULL; // The NULL pointer marks the end of array
  ship[1].propulsor[0] = ship[1].body[5];
  ship[1].propulsor[1] = ship[1].body[6];
  ship[1].front = ship[1].body[2] -> next;
  ship[1].color = RED;
  ship[1].x = 665.0; ship[1].y = 630.0;
  ship[1].dx = ship[1].dy = 0.0;
  ship[1].status = ALIVE;
  gettimeofday(&(ship[1].time), NULL);
  ship[1].fuel = FUEL;
}

Now write the code to free the memory used by the initialized spaceships:

// Free the memory used by the initialized ships
void destroy_ships(void){
  int i;
  for(i = 0; i < NUMBER_OF_SHIPS; i ++){
    if(ship[i].body != NULL){
      int j;
      for(j = 0; ship[i].body[j] != NULL; j ++)
	destroy_polygon(ship[i].body[j]);
    }
  }
  
}

Call initialize_ships(2); in src/game.c right after the call to initialize_star();. And put a call to destroy_ships() right before the destroy_star() in the main function. The next step is write the code to film (draw) and erase the spaceships in the screen. How we already set the camera and the background surface correctly, this is easy:

// Film initialized ships with a camera passed as argument
void film_ships(camera *cam){
  int p, i;
  for(i = 0; i < NUMBER_OF_SHIPS; i ++){
    if(ship[i].body != NULL && ship[i].status == ALIVE){
      for(p = 0; ship[i].body[p] != NULL; p ++){
	film_fullpolygon(cam, ship[i].body[p], BLACK);
	film_polygon(cam, ship[i].body[p], ship[i].color); 
      }
    }
  }
}

// Erase initialized ships with a camera passed as argument
void erase_ships(camera *cam){
  int p, i;
  for(i = 0; i < NUMBER_OF_SHIPS; i ++){
    if(ship[i].body != NULL && ship[i].status == ALIVE){
      for(p = 0; ship[i].body[p] != NULL; p ++){
	erase_fullpolygon(cam, ship[i].body[p]);
	erase_polygon(cam, ship[i].body[p]); 
      }
    }
  }
}

Note that filming a spaceship means films all its polygons. And that for each polygon, first we fill them with the BLACK color and then we draw its perimeter with the ship's color. In src/game.c, in the main loop, call erase_ships(cam) before que input handling and call film_ships(cam) right after update_star().

If everything was done correctly, your game shall be like this: spacewar-1.tar.bz2 (1.5 MB). Feel free to change some constants used in the game, recompile and test the results.

Part 5: Creating the Movement

The pilots inside your spaceships are bored... They press the control buttons, but the ships remain motionless. Let's solve this problem. But first let's erase the camera that we created in src/game.c. We won't need it anymore. In its place, type in the terminal:

weaver cameras

And inside src/cameras.h declare a much improved camera version:

camera *cam[9];

void initialize_cameras(void);
void destroy_cameras(void);

To understend why we need 9 cameras, take a look in the code to initialize them:

void initialize_cameras(void){
  int i;
  for(i = 0; i < 9; i ++){
    cam[i] = new_camera((i < 3)?(-800.0):((i < 6)?(0.0):(800.0)),
			(i%3 == 0)?(-800.0):((i%3 == 2)?(0.0):(800.0)),
			800.0, 800.0);
    limit_camera(cam[i], window_width / 2 - 400, window_height / 2 - 400,
		 800, 800);
  }
}

There's 9 cameras: northweast, west, southwest, north, central, south, northeast, east and southeast one. All them project the images in the 800x800 square in the center of the screen. In our game, the central camera will do almost all the work. The other ones will project images only when a polygon cross the main area boundaries. Their image combined will form the illusion than when a polygon goes too far to east, it appears again at west. The same for all cardinal and intercardinal directions.

This is the code to destroy all the cameras:

void destroy_cameras(void){
  int i;
  for(i = 0; i < 9; i ++)
    destroy_camera(cam[i]);
}

Call the initialization function before the main loop and the destruction function after the main loop in src/game.c.

The functions film_ships() and erase_ships() also need some modification:

// Film initialized ships (second version)
void film_ships(void){
  int p, i, j;
  for(j = 0; j < 9; j++){
    for(i = 0; i < NUMBER_OF_SHIPS; i ++){
      if(ship[i].body != NULL && ship[i].status == ALIVE){
	for(p = 0; ship[i].body[p] != NULL; p ++){
	  film_fullpolygon(cam[j], ship[i].body[p], BLACK);
	  film_polygon(cam[j], ship[i].body[p], ship[i].color);
	}
      }
    }
  }
}
    
// Erase initialized ships (second version)
void erase_ships(void){
  int p, i, j;
  for(j = 0; j < 9; j ++){
    for(i = 0; i < NUMBER_OF_SHIPS; i ++){
      if(ship[i].body != NULL && ship[i].status == ALIVE){
	for(p = 0; ship[i].body[p] != NULL; p ++){
	  erase_fullpolygon(cam[j], ship[i].body[p]);
	  erase_polygon(cam[j], ship[i].body[p]);
	}
      }
    }
  }
}

Don't forget to change how you call these function in src/game.c.

Finally the functions to really create movement. Declare them in src/ships.h:

void update_ships(void);
void rotate_ship(int, int);
void propulse_ship(int);

update_ships() is the function responsible for updating the spaceships position based in each ship's slope. It also correct the ship's position if it goes outside the screen:

// Updates each initialized ship position, based in the ship's slope
void update_ships(void){
  int i;
  for(i = 0; i < NUMBER_OF_SHIPS; i ++){
    if(ship[i].body != NULL && ship[i].status == ALIVE){
      float dx = ship[i].dx / fps;
      float dy = ship[i].dy / fps;
      int j;
      for(j = 0; ship[i].body[j] != NULL; j ++)
	move_polygon(ship[i].body[j], dx, dy);
      ship[i].x += dx;
      ship[i].y += dy;
      if(ship[i].x < 0.0){
	for(j = 0; ship[i].body[j] != NULL; j ++)
	  move_polygon(ship[i].body[j], 800.0, 0.0);
	ship[i].x += 800.0;
      }
      else if(ship[i].x > 800.0){
	for(j = 0; ship[i].body[j] != NULL; j ++)
	  move_polygon(ship[i].body[j], -800.0, 0.0);
	ship[i].x -= 800.0;
      }
      if(ship[i].y < 0.0){
	for(j = 0; ship[i].body[j] != NULL; j ++)
	  move_polygon(ship[i].body[j], 0.0, 800.0);
	ship[i].y += 800.0;
      }
      else if(ship[i].y > 800.0){
	for(j = 0; ship[i].body[j] != NULL; j ++)
	  move_polygon(ship[i].body[j], 0.0, -800.0);
	ship[i].y -= 800.0;
      }
    }
  } 
}

Put a call to update_ships in src/game.c right after the input handling. Then, write the code do rotate the ships and use the propulsion:

// Rotates a given ship to LEFT of RIGHT Pi/2 radians per second
void rotate_ship(int i, int direction){
  if(ship[i].body != NULL && ship[i].status == ALIVE){
    int j;
    float rotation = ((direction == LEFT)?(-SHIP_TURN/fps):(SHIP_TURN/fps));
    for(j = 0; ship[i].body[j] != NULL; j ++)
      rotate_polygon(ship[i].body[j], ship[i].x, ship[i].y, rotation);
  }
}

// Activate spaceship propulsors
void propulse_ship(int i){
  if(ship[i].fuel > 0.0 && ship[i].status == ALIVE){  
    if(ship[i].body != NULL && ship[i].fuel > 0.0 && ship[i].status == ALIVE){
      ship[i].dx += ((ship[i].front -> x - ship[i].x)/20.0) * SHIP_ACCEL / 
        fps;
      ship[i].dy += ((ship[i].front -> y - ship[i].y)/20.0) * SHIP_ACCEL / 
        fps;
      ship[i].fuel -= 100.0 / (float) fps;
    }
  }
}

According with the constants defined by us, the spaceships will turn 90 degrees per second, will have fuel enough to accelerate during 16.5 seconds and the ship's acceleration is 50 pixels per second squared. To use these functions, update the input handling in the main loop inside src/game.c:

    get_input(); 
    if(keyboard[ESC]){
      break;
    }
    // Player 1 moves
    if(keyboard[LEFT])
      rotate_ship(0, LEFT); 
    if(keyboard[RIGHT])
      rotate_ship(0, RIGHT);
    if(keyboard[UP])
      propulse_ship(0);

    // Player 2 moves
    if(keyboard[A])
      rotate_ship(1, LEFT);
    if(keyboard[D])
      rotate_ship(1, RIGHT);
    if(keyboard[W])
      propulse_ship(1);

Now your spaceships are ready to cross the space. If everything was done correctly, your game currently have this format: spacewar-2.tar.bz2 (1.5MB).

Part 6: Creating Particles

In a game, particles are usually little pixels drawn in the screen in big quantities to simulate effects like fire, dust, smog and explosions. In our game, particles are useful to create a special effect when a spaceship uses propulsion and in later explosions.

To create our particles, create first a new module:

weaver dust

Inside src/dust.h, declare:

// How many microseconds a dust particle exists in the screen
#define DUST_LIFE 1000000

struct dust{
  polygon *body;
  float dx, dy;
  unsigned color;
  struct timeval time;
  struct dust *next, *previous;
} *list_of_dust;

void initialize_dust(void);
void destroy_dust(void);
void new_dust(float, float, float, float, unsigned);
void update_dust(void);
void film_dust(void);
void erase_dust(void);

Here our particles are implemented as a linked list of dust. Being the dust a structure composed by a polygon as body (with a coordinate x, y), a slope (dx and dy), a color, a time struct to store the particle age and pointers for other dusts to for our linked list.

The functions initialize_dust and destroy_dust should be called in the beginning and in the end of game respectively. They prepare our data structure to be utilized:

void initialize_dust(void){
  list_of_dust = NULL;
}

// Free memory allocated for the dust particles
void destroy_dust(void){
  struct dust *pointer = list_of_dust;
  while(pointer != NULL){
    if(pointer -> next != NULL){
      pointer = pointer -> next;
      free(pointer -> previous);
    }
    else{
      free(pointer);
      pointer = list_of_dust = NULL;
    }
  }
}

The function new_dust is more complex because it needs to create a new dust and place it in our linked list:

// Creates a new dust particle at coordinates (x, y) with the given
// slope (dx, dy) and with the given color
void new_dust(float x, float y, float dx, float dy, unsigned color){
  struct dust *new_dust = (struct dust *) malloc(sizeof(struct dust));
  // Initializing some values
  new_dust -> body = new_polygon(1, x, y);
  new_dust -> dx = dx;
  new_dust -> dy = dy;
  new_dust -> color = color;
  gettimeofday(&(new_dust -> time), NULL);
  new_dust -> previous = new_dust -> next = NULL;

  // Putting the new dust in list_of_dust:
  if(list_of_dust == NULL){
    list_of_dust = new_dust;
  }
  else{
    struct dust *pointer = list_of_dust;
    while(pointer -> next != NULL)
      pointer = pointer -> next;
    pointer -> next = new_dust;
    new_dust -> previous = pointer;
  }
}

The functions erase_dust and film_dust shall be called respectively after the input handling and the end of the main loop. They respectively erase and draw the particles in the screen:

void film_dust(void){
  int i;
  struct dust *p;
  for(i = 0; i < 9; i++){
    p = list_of_dust;
    while(p != NULL){
      film_polygon(cam[i], p -> body, p -> color);
      p = p -> next;
    }
  }
}

void erase_dust(void){
  int i;
  struct dust *p = list_of_dust;
  for(i = 0; i < 9; i++){
    p = list_of_dust;
    while(p != NULL){
      erase_polygon(cam[i], p -> body);
      p = p -> next;
    }
  }
}

Finally, update_dust is responsible for moving each dust particle and destroy the ones that are too old or touch the star in the center of screen:

// Move each dust particle and destroy the ancient ones and the
// particles that touch the star in the center of screen
void update_dust(void){
  struct dust *pointer = list_of_dust;
  struct dust *temp;
  struct timeval now;
  unsigned long elapsed_microseconds;
  float distance;

  while(pointer != NULL){
    // Moving
    move_polygon(pointer -> body, pointer -> dx / fps, pointer -> dy / fps);
    // Computing the distance:
    distance = pointer -> dx - 400.0;
    distance *= distance;
    distance += (pointer -> dy - 400.0) * (pointer -> dy - 400.0);
    distance = sqrtf(distance);
    // Computing the age:
    gettimeofday(&now, NULL);
    elapsed_microseconds = (now.tv_usec - pointer -> time.tv_usec) +
      1000000 * (now.tv_sec - pointer  -> time.tv_sec);
    // Erasing if the dust is too old or next the star:
    if(elapsed_microseconds > DUST_LIFE || distance < star.size){
      if(pointer -> previous == NULL){ // The first dust
	if(pointer -> next == NULL){ // It's the only dust
	  free(pointer);
	  list_of_dust = NULL;
	  return;
	}
	else{ // We are in the first, but there's others
	  pointer = pointer -> next;
	  free(pointer -> previous);
	  pointer -> previous = NULL;
	  list_of_dust = pointer;
	  continue;
	}
      }
      else{ // We aren't in the first dust
	if(pointer -> next == NULL){ // But we are in the last
	  pointer -> previous -> next = NULL;
	  free(pointer);
	  return;
	}
	else{ // Not the first, nor the last
	  pointer -> previous -> next = pointer -> next;
	  pointer -> next -> previous = pointer -> previous;
	  temp = pointer;
	  pointer = pointer -> previous;
	  free(temp);
	  continue;
	}
      }
    }
    pointer = pointer -> next;
  }
}

update_dust() should be called rightly after or before update_ships() in the main loop, inside src/game.c.

All this won't have any effect if you don't generate the dust when a spaceship uses its propulsors. Correct this rewriting the function propulse_ship in src/ship.c:

// Activate a given ships' propulsors (second version)
void propulse_ship(int i){
  int prop;
  if(ship[i].fuel > 0.0){
    if(ship[i].body != NULL && ship[i].fuel > 0.0){
      ship[i].dx += ((ship[i].front -> x - ship[i].x)/20.0) * SHIP_ACCEL / 
	fps;
      ship[i].dy += ((ship[i].front -> y - ship[i].y)/20.0) * SHIP_ACCEL / 
	fps;
      ship[i].fuel -= 100.0 / (float) fps;
      // Creating the dust:
      for(prop = 0; prop < PROPULSORS; prop ++)
	if(ship[i].propulsor[prop] != NULL){
	  polygon *point, *start;
	  point = start = ship[i].propulsor[prop];
	  do{
	    if(rand()%3){
	      new_dust(point -> x, point -> y,
		       -((ship[i].front -> x - ship[i].x) / 15.0) 
		       * SHIP_ACCEL / fps,
		       -((ship[i].front -> y - ship[i].y) / 15.0) 
		       * SHIP_ACCEL / fps, WHITE);
	    }
	    if(rand()%3){
	      new_dust((point -> x + point -> next -> x)/2, 
		       (point -> y + point -> next -> y)/2,
		       -((ship[i].front -> x - ship[i].x) / 15.0) 
		       * SHIP_ACCEL / fps,
		       -((ship[i].front -> y - ship[i].y) / 15.0) 
		       * SHIP_ACCEL / fps, WHITE);
	    }
	    point = point -> next;
	  } while(point != start); 
	}
    }
  }
}

At this point, if you recompile the game, the spaceships will leave behind a cloud of dust each time the propulsors are utilized. If everything was done correctly, your game shall be like this: spacewar3.tar.bz2 (1.5 MB).

Part 7: Beginning the War

Now that we have particles, it's easier to achieve effects like explosions, shots and things like these. Let's begin programming the spaceship's explosion. First download the file explosion.ogg (24 KB) uploaded by the user "Tcpp" at Wikimedia Commons under Public Domain. Save the file in the sound/ directory. Then, define and declare in src/ships.h:

#define BLOW_UP 10.0

void blow_up_vertex(int, float, float, float, float, float, float, unsigned);
void blow_up(int);

The constant BLOW_UP represents the explosion strenght. How fast the particles created by explosions will fly.

The blow_up_vertex function creates explosion fragments in a given polygon's vertex. The number of fragments created is 2n+1, being n the first argument. The four next arguments define the vertex, passing it's edge coordinates. The sixth and seventh arguments are the explosion center coordinates. It's used to compute the slope of the particles created. The last argument is the color of each particle created in the explosion:

// Blows up a vertex creating 2**(n+1) particles. The vertex is limited by
// the edges (x1, y1) and (x2, y2). The coordinate (bx, by) is the
// center of the explosion:
void blow_up_vertex(int n, float x1, float y1, float x2, float y2, float bx,
		    float by, unsigned color){
  // The recursion end:
  if(n <= 0){
    float distance, dx, dy;
    float x = (x1 + x2) / 2;
    float y = (y1 + y2) / 2;
    // Computing the point's distance to the ship gravity center:
    distance = bx - x;
    distance *= distance;
    distance += (by - y) * (by - y);
    distance = sqrtf(distance);
    // Computing the explosion srenght:
    dx = - ((bx - x) / distance * distance) * BLOW_UP;
    dy = - ((by - y) / distance * distance) * BLOW_UP;
    new_dust(x, y, dx, dy, color);
    new_dust(x, y, dx / 2, dy / 2, color);
  }
  else{
    blow_up_vertex(n-1, x1, y1, (x1 + x2)/2, (y1 + y2)/2, bx, by, color);
    blow_up_vertex(n-1, (x1 + x2)/2, (y1 + y2)/2, x2, y2, bx, by, color);
  }
}

And the function blow_up is responsible to create the visual effects when a spaceship is destroyed. It's the function that will be called when a spaceship is shot or touches the star in the center of screen:

// Blows up a given ship
void blow_up(int i){
  int j;
  polygon *point, *first;
  if(ship[i].body != NULL && ship[i].status == ALIVE){
    ship[i].status = DEAD;
    ship[i].fuel = 0.0;
    gettimeofday(&(ship[i].time), NULL);
    for(j = 0; ship[i].body[j] != NULL; j ++){
      point = first = ship[i].body[j];
      do{
	blow_up_vertex(2, point -> x, point -> y, 
		       point -> next -> x, point -> next -> y,
		       ship[i].x, ship[i].y, ship[i].color);
	point = point -> next;
      }while(point != first);
    }
    play_sound("explosion.ogg");
  }
}

Now let's program the shots. Because isn't very useful when we have the code to explode a spaceship, but in our game there's nothing able to explode them. Create a new module:

weaver shot

And in src/shot.h declare:

// How much microseconds a shot exists
#define SHOT_LIFE 2500000
// The shot size (radius)
#define SHOT_SIZE 3.0
// How much time it takes to produce a shot (microseconds)
#define SHOT_PRODUCTION 500000
// The shot speed:
#define SHOT_SPEED 150.0

struct shot{
  circle *body;
  float dx, dy;
  struct timeval time;
  struct shot *next, *previous;
  unsigned color;
  int owner;
};

struct shot *list_of_shot;

void initialize_shot(void);
void destroy_shot(void);
void new_shot(float, float, float, float, unsigned, int);
int shot_hits(struct shot *);
void update_shot(void);
void film_shot(void);
void erase_shot(void);

The shot code is similar to the dust code. The difference is in the shape (a dust is a point, a shot is a circle) and shots have an additional variable to store it's owner. A shot can't destroy the ship that create it, only enemy ships.

A shot is a circle with 3 pixels of radius and disappears after 2.5 seconds. A spaceship can fire 2 shots per second.

As in the dust code, we need to initialized the linked list and after destroy it. In the main function, you should call these functions next to the dust ones:

void initialize_shot(void){
  list_of_shot = NULL;
}

// Free memory allocated for the shots
void destroy_shot(void){
  struct shot *pointer = list_of_shot;
  while(pointer != NULL){
    if(pointer -> next != NULL){
      pointer = pointer -> next;
      free(pointer -> previous);
    }
    else{
      free(pointer);
      pointer = list_of_shot = NULL;
    }
  }
}

The new_shot function takes an additional argument compared to new_dust because it needs to store it's owner. There's also some differences while we initialize the body. The shot is a circle, not a single point:

// Creates a new shot at coordinates (x, y) with the given
// slope (dx, dy), the given color and the given owner
void new_shot(float x, float y, float dx, float dy, unsigned color, int owner){
  struct shot *new_shot = (struct shot *) malloc(sizeof(struct shot));
  // Initializing some values
  new_shot -> body = new_circle(x, y, SHOT_SIZE);
  new_shot -> dx = dx;
  new_shot -> dy = dy;
  new_shot -> color = color;
  gettimeofday(&(new_shot -> time), NULL);
  new_shot -> previous = new_shot -> next = NULL;
  new_shot -> owner = owner;

  // Putting the new shot in list_of_shot:
  if(list_of_shot == NULL){
    list_of_shot = new_shot;
  }
  else{
    struct shot *pointer = list_of_shot;
    while(pointer -> next != NULL)
      pointer = pointer -> next;
    pointer -> next = new_shot;
    new_shot -> previous = pointer;
  }
}

The function shot_hits does the collision detection between shots and spaceships and also explode a ship if it collides with a shot. It returns 1 in case of collision and 0 otherwise:

// Determines if a shot hits the enemy
int shot_hits(struct shot *bullet){
  int i, j;
  for(i = 0; i < NUMBER_OF_SHIPS; i ++){
    if(i == bullet -> owner)
      continue;
    for(j = 0; ship[i].body[j] != NULL; j ++){
      if(collision_circle_polygon(bullet -> body, ship[i].body[j])){
	blow_up(i);
	return 1; 
      }
    }
  }
  return 0;
}

The function update_shot is responsible for moving all the shots, destroy the shot if it's too old or if it touches the star, and also to detect collision and explode hitten spaceships each iteration in the main loop. It should be placed next to the update_dust in the main loop.

// Move each shot particle and destroy the ancient ones and the
// particles that touch the star in the center of screen
void update_shot(void){
  struct shot *pointer = list_of_shot;
  struct shot *temp;
  struct timeval now;
  unsigned long elapsed_microseconds;
  float distance;

  while(pointer != NULL){
    // Moving
    move_circle(pointer -> body, pointer -> dx / fps, pointer -> dy / fps);
    if(pointer -> body -> x < 0.0) pointer -> body -> x += 800.0;
    if(pointer -> body -> x > 800.0) pointer -> body -> x -= 800.0;
    if(pointer -> body -> y < 0.0) pointer -> body -> y += 800.0;
    if(pointer -> body -> y > 800.0) pointer -> body -> y -= 800.0;

    shot_hits(pointer);
    // Computing the distance:
    distance = pointer -> dx - 400.0;
    distance *= distance;
    distance += (pointer -> dy - 400.0) * (pointer -> dy - 400.0);
    distance = sqrtf(distance);
    
    // Erasing if the shot is too old
    gettimeofday(&now, NULL);
    elapsed_microseconds = (now.tv_usec - pointer -> time.tv_usec) +
      1000000 * (now.tv_sec - pointer  -> time.tv_sec);
    if(elapsed_microseconds > SHOT_LIFE || distance < star.size){
      if(pointer -> previous == NULL){ // The first shot
	if(pointer -> next == NULL){ // It's the only shot
	  free(pointer);
	  list_of_shot = NULL;
	  return;
	}
	else{ // We are in the first, but there's others
	  pointer = pointer -> next;
	  free(pointer -> previous);
	  pointer -> previous = NULL;
	  list_of_shot = pointer;
	  continue;
	}
      }
      else{ // We aren't in the first shot
	if(pointer -> next == NULL){ // But we are in the last
	  pointer -> previous -> next = NULL;
	  free(pointer);
	  return;
	}
	else{ // Not the first, nor the last
	  pointer -> previous -> next = pointer -> next;
	  pointer -> next -> previous = pointer -> previous;
	  temp = pointer;
	  pointer = pointer -> previous;
	  free(temp);
	  continue;
	}
      }
    }
    pointer = pointer -> next;
  }
}

And the last functions are the responsible for erasing and drawing the shots in the screen. You should call these functions in the mains loop next to erase_dust and film_dust:

void film_shot(void){
  int i;
  struct shot *p;
  for(i = 0; i < 9; i++){
    p = list_of_shot;
    while(p != NULL){
      film_fullcircle(cam[i], p -> body, p -> color);
      p = p -> next;
    }
  }
}

void erase_shot(void){
  int i;
  struct shot *p = list_of_shot;
  for(i = 0; i < 9; i++){
    p = list_of_shot;
    while(p != NULL){
      erase_fullcircle(cam[i], p -> body);
      p = p -> next;
    }
  }
}

You described here all the logic that drives the shots. But the spaceships still need an aditional function to fire the shots. In src/ships.h and src/ships.c, declare and define a new function:

void ship_fire(int);
// Makes the given spaceship fire
void ship_fire(int i){
  if(ship[i].status == ALIVE){
    unsigned long elapsed_microseconds;
    struct timeval now;

    gettimeofday(&now, NULL);
    elapsed_microseconds = (now.tv_usec - ship[i].time.tv_usec) +
      1000000 * (now.tv_sec - ship[i].time.tv_sec);
    if(elapsed_microseconds > SHOT_PRODUCTION){
      gettimeofday(&(ship[i].time), NULL);
      new_shot(ship[i].front -> x, ship[i].front  -> y,
	       ((ship[i].front -> x - ship[i].x) / 15.0) 
	       * SHOT_SPEED,
	       ((ship[i].front -> y - ship[i].y) / 15.0) 
	       * SHOT_SPEED, ship[i].color, i);
    }
  }
}

And finally, we update the main loop in src/game.c in the input handling to associate the fire action with the buttons DOWN and S:

    if(keyboard[DOWN])
      ship_fire(0);
    if(keyboard[S])
      ship_fire(1);

If everything was one correctly your game shall be like this: spacewar4.tar.bz2 (1.5 MB). Now the ships can attack each other and be destroyed.

Part 8: The Star and the Gravity

In this part we're going to explode the spaceships that touch the star and also program the star deadly gravity. If you don't want this effect in tour game, you can skip this part.

First the star needs to became dangerous. A spaceship must explode if it touches the star and the gravity shall attract the ships. The gravity shall also attract the shots and the dust particles. To achieve this, declare in src/star.h the functions:

// Forward declaration
struct dust;
struct shot;

int gravity_to_ship(int);
int gravity_to_dust(struct dust *);
int gravity_to_shot(struct shot *);

These functions shall check the distance between these elements and the center of the screen. If it's lesser than the star size, it returns 0. Else, it updates the dx and dy variables and return 1.

This is gravity_to_ship function:

// Uses gravitational attraction in the given ship
int gravity_to_ship(int i){
  float strenght;
  // Computing the distance:
  float distance = ship[i].x - 400.0;
  distance *= distance;
  distance += (ship[i].y - 400.0) * (ship[i].y - 400.0);
  distance = sqrtf(distance);
  if(distance < star.size){
    blow_up(i);
    return 0;
  }
  // Applying the gravity:
  strenght = STAR_GRAV / (fps * (distance * distance));
  ship[i].dx += ((400.0 - ship[i].x) / distance) * strenght;
  ship[i].dy += ((400.0 - ship[i].y) / distance) * strenght;
  return 1;
}

The best place to call this function is inside the block executed if the spaceship is ALIVE in function update_ships inside src/ships.c.

This is the gravity_to_dust function:

// Uses gravitational attraction in the given dust
int gravity_to_dust(struct dust *p){
  float strenght;
  // Computing the distance:
  float distance = p -> body -> x - 400.0;
  distance *= distance;
  distance += (p -> body -> y - 400.0) * (p -> body -> y - 400.0);
  distance = sqrtf(distance);
  if(distance < star.size)
    return 0;
  // Applying the gravity:
  strenght = STAR_GRAV / (fps * (distance * distance));
  p -> dx += ((400.0 - p -> body -> x) / distance) * strenght;
  p -> dy += ((400.0 - p -> body -> y) / distance) * strenght;
  return 1;
}

This function should be called for each dust particle in the inner loop inside update_dust function in src/dust.c. If you wish, you can also upgrade the code using the return value of gravity_to_dust to determine if each given particle shall be destroyed.

The function gravity_to_shot is almost the same thing and follow the same logic. Should be placed inside update_shot:

// Uses gravitational attraction in the given shot
int gravity_to_shot(struct shot *p){
  float strenght;
  // Computing the distance:
  float distance = p -> body -> x - 400.0;
  distance *= distance;
  distance += (p -> body -> y - 400.0) * (p -> body -> y - 400.0);
  distance = sqrtf(distance);
  if(distance < star.size)
    return 0;
  // Applying the gravity:
  strenght = STAR_GRAV / (fps * (distance * distance));
  p -> dx += ((400.0 - p -> body -> x) / distance) * strenght;
  p -> dy += ((400.0 - p -> body -> y) / distance) * strenght;
  return 1;
}

If everything was done correctly, your game should be like this: spacewar5.tar.bz2 (1.5MB).

Part 9: Exploring Hyperspace

The hyperspace, in science fiction literature, is another dimension to where is possible to travel using a large ammount of energy, and it's an evidence that the universe's space is non-euclidean. Travel to hyperspace is very dangerous, but if you take the risk, you can take shortcuts to other places in the universe, what gives the impression that you were faster than light. To an observer in the traditional space, it's like teletransport.

In our game, travel to hyperspace can make our spaceship disappear from the conventional space for 1 second. While a spaceship is in hyperspace, it cannot be destroyed. But the travel takes 165 units of fuel. To reflect this, define in src/ships.h:

#define HYPERSPACE_TIME 1000000lu
#define HYPERSPACE_FUEL 165.0

Going to hyperspace also does a caracteristic sound. You can download the noise and put in sound/ directory: hyper.ogg (14KB).
(Copyright 2012 Iwan 'qubodup' Gabovitch http://qubodup.net qubodup@gmail.com | License: CC Attribution-ShareAlike 3.0 Unported http://creativecommons.org/licenses/by-sa/3.0/).

In terms of code, we'll need one function to send a spaceship to hyperspace and another one to returns a ship from there. Declare these functions in src/ships.h:

void goto_hyperspace(int);
void return_hyperspace(int);

In terms of code, going to hyperspace is changing a spaceship's state, moving it to some random place and making noise:

// Sends a given ship to hyperspace
void goto_hyperspace(int i){
  if(ship[i].status == ALIVE && ship[i].fuel >= HYPERSPACE_FUEL){
    int j;
    float dx = 400.0 - (float) (rand() % 800);
    float dy = 400.0 - (float) (rand() % 800);
    gettimeofday(&(ship[i].time), NULL);
    ship[i].status = HYPERSPACE;
    for(j = 0; ship[i].body[j] !=  NULL; j++){
      move_polygon(ship[i].body[j], dx, dy);
    }
    ship[i].x += dx;
    ship[i].y += dy;
    ship[i].fuel -= HYPERSPACE_FUEL;
    play_sound("hyper.ogg");
  }
}

Returning from there is even easier:

// Returns a given ship from hyperspace
void return_hyperspace(int i){
  if(ship[i].status == HYPERSPACE){
    gettimeofday(&(ship[i].time), NULL);
    ship[i].status = ALIVE;
  }
}

The function return_hyperspace() shall be called automaticly if a spaceship spent 1 second in hyperspace. To achieve this, we'll create a new version of function update_ships():

// Updates each initialized ship position, based in the ship's slope
// Version 2
void update_ships(void){
  int i;
  for(i = 0; i < NUMBER_OF_SHIPS; i ++){
    if(ship[i].body != NULL && ship[i].status == ALIVE){
      float dx = ship[i].dx / fps;
      float dy = ship[i].dy / fps;
      int j;
      for(j = 0; ship[i].body[j] != NULL; j ++)
	move_polygon(ship[i].body[j], dx, dy);
      ship[i].x += dx;
      ship[i].y += dy;
      if(ship[i].x < 0.0){
	for(j = 0; ship[i].body[j] != NULL; j ++)
	  move_polygon(ship[i].body[j], 800.0, 0.0);
	ship[i].x += 800.0;
      }
      else if(ship[i].x > 800.0){
	for(j = 0; ship[i].body[j] != NULL; j ++)
	  move_polygon(ship[i].body[j], -800.0, 0.0);
	ship[i].x -= 800.0;
      }
      if(ship[i].y < 0.0){
	for(j = 0; ship[i].body[j] != NULL; j ++)
	  move_polygon(ship[i].body[j], 0.0, 800.0);
	ship[i].y += 800.0;
      }
      else if(ship[i].y > 800.0){
	for(j = 0; ship[i].body[j] != NULL; j ++)
	  move_polygon(ship[i].body[j], 0.0, -800.0);
	ship[i].y -= 800.0;
      }
      gravity_to_ship(i);
    }
    else if(ship[i].body != NULL && ship[i].status == HYPERSPACE){
      /* If we spent too much time in hyperspace, came back. */
      struct timeval now;
      unsigned long elapsed_microseconds;
      gettimeofday(&now, NULL);
      elapsed_microseconds = (now.tv_usec - ship[i].time.tv_usec) +
	1000000 * (now.tv_sec - ship[i].time.tv_sec);
       if(elapsed_microseconds > HYPERSPACE_TIME){
	return_hyperspace(i);
      } 
    } 
  }
}

Now, in the main loop, we should make some keys send a spaceship to hyperspace. After the changes, the input handling should be like this:

    get_input(); 
    if(keyboard[ESC]){
      break;
    }
    // Player 1 moves
    if(keyboard[LEFT])
      rotate_ship(0, LEFT); 
    if(keyboard[RIGHT])
      rotate_ship(0, RIGHT);
    if(keyboard[UP])
      propulse_ship(0);
    if(keyboard[DOWN])
      ship_fire(0);
    if(keyboard[RIGHT_CTRL])
      goto_hyperspace(0);

    // Player 2 moves
    if(keyboard[A])
      rotate_ship(1, LEFT);
    if(keyboard[D])
      rotate_ship(1, RIGHT);
    if(keyboard[W])
      propulse_ship(1);
    if(keyboard[L])
      blow_up(1);
    if(keyboard[S])
      ship_fire(1);
    if(keyboard[LEFT_CTRL])
      goto_hyperspace(1);

If everything was done correctly, our game should be like this: spacewar6.tar.bz2 (1.6MB).

Part 10: Final Details

The game is almost done. But there's still some minor details to improve the gaming experience. First, we should play some music during the battle. Something like the Battle Music "Escape of the Dreadnought", created by "Sindwiller" licenced under GNU GPL 3.0 and available for download HERE (969 KB) and in OpenGameArt.org.

You should put the music file in music/ and call before the main loop:

    play_music("music.ogg");

And after the main loop:

    stop_music();

Other important step is draw in the screen some hint about how much fuel each spaceship have. To do this, you'll need the following PNG files to draw each ship fuel tank:

Save the 2 images as images/empty_tank.png and images/full_tank.png. The idea is always draw the empty tank first, and then draw a part of the full tank in the same position according with the ammount of fuel.

The function to draw the fuel tanks should be declared in src/ships.h:

void draw_tank(int, int, int);

And defined in src/ships.c:

// This function draws the fuel tank associated with a given spaceship
void draw_tank(int i, int x, int y){
  surface *tank = new_image("empty_tank.png");
  draw_surface(tank, window, x, y);
  destroy_surface(tank);
  if(ship[i].fuel <= 0)
    return;
  tank = new_image("full_tank.png");
  int height = 300 - 3 * (int) (ship[i].fuel / (FUEL / 100.0));
  draw_rectangle_mask(tank, 0, 0, tank -> width, height);
  draw_surface(tank, window, x, y);
  destroy_surface(tank);
}

This function can be invoked in the end of the main loop. Or, to be more efficient, it should be invoked after every event that changes the ammount of fuel. After the beginning of a new game, after some spaceship uses its propulsor, or after some spaceship goes to hyperspace.

In the end, your game shoud look like this: spacewar7.tar.bz2 (2.5 MB).

Of corse, the game isn't finished. Finished games doesn't exist, there's always space for improvemeent. We could create an AI to control some spaceship and play against the other spaceship controlled by a human. We could create other scenarios (like a bigger battlefield with lots o asteroids). We could create other spaceships. We could create more game modes and allow more than 2 players. We could create a function to detect if the game is over and a main menu. Or we could improve the game performance.

But this tutorial will end now because the game core was already programmed and we already programmed every feature listed in the first part, where the game was planned. Feel free to play with the source code and create new elements in the little universe created here. Happy hacking. :-)

THE END


XHTML 1.1 válido! CSS válido!