// Workshop 7 part 1 import sdljava.*; import sdljava.video.*; import sdljava.event.*; import sdljava.ttf.*; import java.util.LinkedList; import java.util.ListIterator; // This is a general purpose vector class class c_vector { // Constructor that takes another vector as input public c_vector( c_vector value ) { x = value.x; y = value.y; } // Constructor that takes two floats as input public c_vector( float a, float b ) { x = a; y = b; } // Constructor that takes one float as input public c_vector( float value ) { x = y = value; } // Constructor that takes no input public c_vector() { x = y = 0.0f; } // Normalises the vector public void normalise() { // Retrieve the magnitude float len = get_length(); if( len > 0.0f ) { // Normalise the vector by dividing the components by this magnitude x /= len; y /= len; } else // This should stop divide by zero errors x = y = 0.0f; } // Returns the length of the vector public float get_length() { // Return the magnitude (length) using Pythagoras' theorem return (float) Math.sqrt( (x*x) + (y*y) ); } // Our vector's components public float x, y; } class game_object { // Static function for storing the screen size public static void set_screen_size(c_vector screenIn) { // This will be used in the update functions, so see if the object collides with the screen screen = screenIn; } // Safe method for (eventually) adding an object to the main object list (similar in concept to the blacklist flag) public static void add_game_object(game_object in) { // We will be keeping an internal static store of all objects in a temporary list // If this list does not exist.. if( object_list == null ) { // .. create it! object_list = new LinkedList<game_object>(); } // Add the object to the list object_list.add(in); } public static LinkedList<game_object> get_game_object_list() { // We need to do two things here: return our list, and set the variable to null // To do this, we declare a temporary variable to hold the list LinkedList<game_object> temp = object_list; // Then set the object list to null so that we can create a new (empty) one next time we need to add something object_list = null; // And finally, return the actual list return temp; } public game_object() { position = new c_vector(); size = new c_vector(); direction = new c_vector(); speed = 0; image = null; blacklist = false; } // Adds the object to the blacklist (i.e. flags that it should be removed) public void blacklist_add() { blacklist = true; } // Queries whether the object is on the blacklist public boolean blacklist_get() { return blacklist; } // For setting the movement direction of the object public void set_direction(c_vector in) { // The direction vector is a normalised unit vector // We don't know what the vector we've been given is, so we will normalise it in.normalise(); direction = in; } public boolean update(float deltaT_s) { // Update the position position.x += deltaT_s * direction.x * speed; position.y += deltaT_s * direction.y * speed; // Test against the sides of the screen, keeping track of whether it hits boolean hits_sides = false; if( position.x - (size.x/2) < 0.0f ) { position.x = size.x/2; hits_sides = true; } else if( position.x + (size.x/2) > screen.x ) { position.x = screen.x - (size.x/2); hits_sides = true; } if( position.y - (size.y/2) < 0.0f ) { position.y = size.y/2; hits_sides = true; } else if( position.y + (size.y/2) > screen.y ) { position.y = screen.y - (size.y/2); hits_sides = true; } // Return whether the object hits the sides of the screen return hits_sides; } public void draw(SDLSurface screenIn) throws SDLException { // Draw the object SDLRect rectangle = new SDLRect((int)position.x-(int)(size.x/2.0f),(int)position.y-(int)(size.y/2.0f),(int)size.x,(int)size.y); image.blitSurface(screenIn,rectangle); } // Performs a basic collision test, returning true if the objects collide protected static boolean collide(game_object a, game_object b) { // This static function performs a basic collision test // It does not decide what to do when two objects collide, as this requires knowledge of the object types if( a.position.x - (a.size.x/2) < b.position.x + (b.size.x/2) && a.position.x + (a.size.x/2) > b.position.x - (b.size.x/2) ) { // If these tests pass, it means that the objects are at least partially vertically aligned // We now need to see if they are also horizontally aligned. // If it is, we will have a collision if( a.position.y - (a.size.y/2) < b.position.y + (b.size.y/2) && a.position.y + (a.size.y/2) > b.position.y - (b.size.y/2) ) { // If these final tests pass, it means that the objects are also horizontally aligned // I.e. they intersect (we have a collision) return true; } } return false; } // Performs and resolves a collision test public void collision_test(game_object in) { // The base game_object class does nothing - this function must be overridden by child objects to do something sensible } protected c_vector position; protected c_vector size; protected c_vector direction; protected float speed; protected SDLSurface image; protected static c_vector screen; protected boolean blacklist; protected static LinkedList<game_object> object_list; } class bullet extends game_object { // Bullet constructor // Java note: the no-arg constructor of the super class will be automatically called before this one. public bullet( game_object originIn, SDLSurface imageIn, c_vector directionIn, c_vector start_pos, float speedIn ) { image = imageIn; size.x = image.getWidth(); size.y = image.getHeight(); direction = directionIn; position = start_pos; speed = speedIn; origin = originIn; } // The bullet's update function public boolean update(float deltaT_s) { // For a bullet, we need to do something very slightly different for the update // Perform the regular update, making a note of whether we hit the sides of the screen boolean hits_sides = super.update(deltaT_s); // If we do hit the sides, we will be going off the screen, so need to be flagged for removal: if( hits_sides ) blacklist_add(); // Still return whether we hit the sides, just for completeness ;) return hits_sides; } // For retrieving the origin object public game_object get_origin() { return origin; } // Performs and resolves a collision test public void collision_test(game_object in) { // We won't bother doing anything ourselves, but will refer the test over to the other object: if( !(in instanceof bullet) ) { // Provided the other objet isn't another bullet, let it perform the collision test: in.collision_test(this); } } // The game object that fired the bullet // This will be useful when we check for collisions, as we won't test against the object that fired it protected game_object origin; } class spaceship extends game_object { // Space ship constructor // Java note: the no-arg constructor of the super class will be automatically called before this one. public spaceship(SDLSurface imageIn, c_vector start_pos, float speedIn, c_vector shoot_directionIn, SDLSurface bullet_imageIn, float bullet_speedIn) { image = imageIn; // Determine our size from the image we have been given size.x = image.getWidth(); size.y = image.getHeight(); position = start_pos; shoot_direction = shoot_directionIn; bullet_image = bullet_imageIn; bullet_speed = bullet_speedIn; speed = speedIn; kills = 0; } // This function is called whenever we want the space ship to fire a bullet public bullet fire() { // Create and return a new bullet return new bullet((game_object)this,bullet_image,shoot_direction,new c_vector(position), bullet_speed ); } // Performs and resolves a collision test public void collision_test(game_object in) { // Call the collision test function if( collide(this,in) ) { // A collision has taken place // What we do now depends on what type the other object is: if( in instanceof bullet ) { // We have collided with a bullet (i.e. we've been shot at!!) // Check that it wasn't our bullet if( ((bullet)in).get_origin() != this ) { // We will create a little explosion, just as a special effect ;) // This is a game_object, as it needs to be updated and drawn, // so we add it to the main list using our safe add_game_object function: add_game_object((game_object)new ss_explosion(new c_vector(in.position),0.3f)); // The bullet is no longer required, as it has found its mark in.blacklist_add(); // This is where we will adjust our health, and possibly get destroyed in a nice big explosion.... } } } } // Notifies the ship that it has killed something public void add_kill() { kills++; } // Retrieves the number of kills public int get_kills() { return kills; } // The direction that new bullets will fire in protected c_vector shoot_direction; // The image to use for new bullets protected SDLSurface bullet_image; // The speed of a new bullet: protected float bullet_speed; // The number of kills that the space ship has made protected int kills; } class enemy extends spaceship { public enemy(SDLSurface imageIn, c_vector start_pos, float speedIn, c_vector shoot_directionIn, SDLSurface bullet_imageIn, float bullet_speedIn, c_vector move_direction, float shoot_timeIn, int healthIn) { // Call the spaceship's constructor super(imageIn, start_pos, speedIn, shoot_directionIn, bullet_imageIn, bullet_speedIn); // Set ourselves moving in the given direction set_direction(move_direction); // The timer will be used for how often we can shoot timer = 0.0f; // The time in seconds between shots shoot_time = shoot_timeIn; // How many hits until we get destroyed health = healthIn; } public boolean update(float deltaT_s) { // Update the timer for how often we shoot timer += deltaT_s; // If the amount of time that has passed (as stored in our timer variable) is greater than our shoot time, // it is time to shoot something! if( timer >= shoot_time ) { // The add_game_object function provides a means of safely adding a new game object to the main list add_game_object((game_object)fire()); // We will also reset the timer timer = 0.0f; } // Again, see if we have hit the sides of the screen boolean hits_sides = super.update(deltaT_s); // If we have hit the sides, we will assume that we have passed off the edge of the screen, and are no longer in game play // So, set our blacklist flag if( hits_sides ) blacklist_add(); return hits_sides; } // Performs and resolves a collision test public void collision_test(game_object in) { // Call the collision test function if( collide(this,in) ) { // A collision has taken place // What we do now depends on what type the other object is: if( in instanceof bullet ) { // We have collided with a bullet (i.e. we've been shot at!!) // Check that it wasn't our bullet if( ((bullet)in).get_origin() != this ) { // We will create a little explosion, just as a special effect ;) // This is a game_object, as it needs to be updated and drawn, // so we add it to the main list using our safe add_game_object function: add_game_object((game_object)new ss_explosion(new c_vector(in.position),0.3f)); // The bullet is no longer required, as it has found its mark in.blacklist_add(); // We are now injured health -= 1; // If our health is zero, we have been destroyed if( health <= 0 ) { // This would be a good place to add a big explosion ;) // Set our blacklist flag, so that we can be removed at a convenient time blacklist_add(); // If the bullet originated from a spaceship... if( ((bullet)in).get_origin() instanceof spaceship ) { // ... we will add to that ship's list of kills ((spaceship)((bullet)in).get_origin()).add_kill(); } } } } } } // Counter used for timing the shooting rate float timer; // The 'recharge time' - i.e. the time between shots float shoot_time; // How many hits until we get destroyed - this will be moved to the spaceship class int health; } // A single sprite explosion class ss_explosion extends game_object { // A static init function for saving the image to use public static void init(SDLSurface imageIn) { explosion_image = imageIn; } public ss_explosion(c_vector positionIn, float durationIn) { position = positionIn; duration = durationIn; image = explosion_image; size.x = image.getWidth(); size.y = image.getHeight(); } // Overridden update function public boolean update(float deltaT_s) { // There isn't much to update for one of these // Count down our duration duration -= deltaT_s; // If our duration has reached zero, remove ourselves if( duration <= 0.0f ) blacklist_add(); // We're not interested in checking whether it has hit the sides of the screen return false; } // How long the sprite will survive protected float duration; // The image to use protected static SDLSurface explosion_image; } class s07_p01 { // Our SDL surface for drawing on static SDLSurface screen; // The player controllable space ship static spaceship my_ship; // The direction that the player controllable ship is moving in according to the user input static c_vector ship_direction; // List that contains all the game objects in our game static LinkedList<game_object> game_objects; // This will store a font static SDLTrueTypeFont main_font; // Timer used in enemy spawning static float enemy_timer; // The image used for enemies static SDLSurface enemy_image; // The image used for enemy bullets static SDLSurface enemy_bullet_image; // We will call this function whenever we want to exit the program. // It uninitialises SDL and forces a program exit. public static void exit(int value) { SDLMain.quit(); System.out.println("Exiting.."); System.exit(value); } // In this function, we retrieve and handle events public static boolean handle_events() { try { // SDL stores events in a queue. // We can either wait for events, in which case the function we call will not return until there is an event, // or we can have a quick look at the event queue, using a function that will retrieve an event if there is one, returning null if there isn't. // In an interactive game, it's not much use if we get stuck waiting for events, as we won't be able to draw or update the scene. // Therefore, we want to use the 'quick look' option. // This is called polling for events. // The waiting technique is known as blocking. // Poll for events SDLEvent event = SDLEvent.pollEvent(); // If there was an event.. if( event != null ) // What we do depends on the type switch( event.getType() ) { case SDLEvent.SDL_QUIT: // This event type is generated when the user clicks on the 'x' to close the window return true; case SDLEvent.SDL_KEYDOWN: // The user has pressed a key { // This means that the event is actually of a sub-type called an SDLKeyboardEvent. // If we cast the event to this type, we can retrieve the actual key type int key = ((SDLKeyboardEvent) event).getSym(); // What we do now depends on the key that was pressed switch( key ) { case SDLKey.SDLK_ESCAPE: exit(0); case SDLKey.SDLK_UP: ship_direction.y += -1.0f; my_ship.set_direction(new c_vector(ship_direction)); break; case SDLKey.SDLK_DOWN: ship_direction.y += 1.0f; my_ship.set_direction(new c_vector(ship_direction)); break; case SDLKey.SDLK_LEFT: ship_direction.x += -1.0f; my_ship.set_direction(new c_vector(ship_direction)); break; case SDLKey.SDLK_RIGHT: ship_direction.x += 1.0f; my_ship.set_direction(new c_vector(ship_direction)); break; case SDLKey.SDLK_SPACE: // This will be the fire button // We call our space ship's fire function // This returns a bullet, which is a game object // To get the game object updated, we need to add it to our list // Since this list dictates the drawing order too, we will add it to the front of the list // Since our ship is at the back, the bullets will be drawn under it. game_objects.addFirst((game_object)my_ship.fire()); break; default: } } break; case SDLEvent.SDL_KEYUP: // The user has released a key { // This means that the event is actually of a sub-type called an SDLKeyboardEvent. // If we cast the event to this type, we can retrieve the actual key type int key = ((SDLKeyboardEvent) event).getSym(); // What we do now depends on the key that was pressed switch( key ) { case SDLKey.SDLK_UP: ship_direction.y -= -1.0f; my_ship.set_direction(new c_vector(ship_direction)); break; case SDLKey.SDLK_DOWN: ship_direction.y -= 1.0f; my_ship.set_direction(new c_vector(ship_direction)); break; case SDLKey.SDLK_LEFT: ship_direction.x -= -1.0f; my_ship.set_direction(new c_vector(ship_direction)); break; case SDLKey.SDLK_RIGHT: ship_direction.x -= 1.0f; my_ship.set_direction(new c_vector(ship_direction)); break; } } break; default: } } catch(SDLException e) { System.err.println("Error while polling event : " + SDLMain.getError()); exit(1); } return false; } // This function will create a new enemy ship public static void create_new_enemy() { // Create our space ship enemy new_enemy = new enemy(enemy_image, // The image to use for the ship new c_vector((int)((float)Math.random()*(screen.getWidth()-enemy_image.getWidth())),enemy_image.getHeight()/2), // The starting position of the ship 100.0f, // The speed of the ship new c_vector(0,1), // The direction that bullets are fired at enemy_bullet_image, // The image to use for the bullets 300.0f, // The speed of the bullets new c_vector(0,1), // The direction that the enemy will move in 1.0f, // The fire rate of the enemy 3); // The number of shots that are required to kill the enemy // Add the space ship to the main game object list game_objects.add((game_object)new_enemy); } // In this function, we update our scene public static void update( float deltaT_s ) { // Update our timer enemy_timer += deltaT_s; // This will be triggered every second if( enemy_timer >= 1.0f ) { // Create a new enemy create_new_enemy(); // Reset our timer enemy_timer = 0.0f; } // Generate an iterator for the first item in the list // (Iterators are useful helper classes that allow us to cycle through a list. // We generate one that starts at the beginning of the list, // and we will use it to retrieve the next item) ListIterator it = game_objects.listIterator(0); // Go through the list while( it.hasNext() ) { // Retrieve the next item game_object current = (game_object) it.next(); // Update the object current.update(deltaT_s); // We now need to perform a collision test using this object against all other objects // However, since our objects are in a list, we can eliminate multiple calls by only testing // against all other objects further up than the current one in the list. // If we didn't do this, we would end up testing each collision twice: // consider a list: A, B, C // If we take object A, and perform tests going through the list: // A vs A (we can easily detect that) // A vs B // A vs C // Okay, fair enough, let's move on to object B: // B vs A whoops, we've already done this! This is the same as A vs B, since the object order doesn't matter // Thus, if we start with the next object in the list, we can remove this problem, and the first test for B becomes: // B vs C // This also has the added bonus that we don't even have to test C! // Create a new iterator starting with the next item in the list ListIterator collisionIt = game_objects.listIterator(it.nextIndex()); // Follow this iterator through to the end of the list while( collisionIt.hasNext() ) { // Retrieve the next object game_object next = (game_object) collisionIt.next(); // Perform the collision test and resolution current.collision_test(next); } // If an object needs to be deleted, it will set an internal 'blacklist' flag // If this flag is set, we need to remove it from our main game objects list. // Why might an object need to be deleted? // There are various reasons, for example: if the object explodes, or if a bullet reaches the edge of the screen if( current.blacklist_get() ) it.remove(); } // During our update, we may have ended up with extra objects that need to be added to our main object list // These were stored in a static list within the game_object class // Let's retrieve that list: LinkedList<game_object> extra_objects = game_object.get_game_object_list(); // And if it isn't empty, add all the objects from it into our main game_objects list: if( extra_objects != null ) game_objects.addAll(extra_objects); // All the extra objects will now be in the main objects list } // Draws a HUD public static void draw_HUD(SDLSurface screenIn) throws sdljava.SDLException { // Generate some text that says how many kills the player has made // To do this, we use the main font, and use it to generate an RGBA (actually, SDL stores it as ARGB) SDLSurface // We can then blit that to the screen as usual SDLSurface kills_text = main_font.renderTextBlended("Kills: "+my_ship.get_kills(), new SDLColor(255,255,255) ); // Rectangle for where we're blitting to // For blitting operations the size is not actually used, just the location (the first two values) SDLRect rectangle = new SDLRect(10,10,0,0); // Blit the kills text to the screen, using the rectangle to specify the location kills_text.blitSurface(screenIn,rectangle); } // In this function, we draw our scene // Upon encountering an error, this function throws an SDLException, which is passed on from SDL function calls public static void draw() throws sdljava.SDLException { // First, we clear the screen screen.fillRect(screen.mapRGB(0,0,0)); // Generate an iterator for the first item in the list ListIterator it = game_objects.listIterator(0); // Go through the list while( it.hasNext() ) { // Retrieve the next object in the list game_object current = (game_object) it.next(); // Draw it to the screen current.draw(screen); } // We will draw the HUD now draw_HUD(screen); screen.flip(); } public static void main(String[] args) { // Create an SDL object SDLMain sdl = new SDLMain(); // SDL uses exceptions when it encounters problems try { // Initialise SDL to use its video (graphics) capabilities sdl.init(SDLMain.SDL_INIT_VIDEO); // SDL uses what it calls a 'surface' as a window. screen = SDLVideo.setVideoMode(640, 480, 0, 0 ); // Tell the game object class what size screen we're using game_object.set_screen_size(new c_vector(640.0f,480.0f)); // Initialise the TTF (True Type Font) utility SDLTTF.init(); // Load a font for use. We have to specify a pixel height for the font. // FreeSans.ttf is a free font available from: http://www.nongnu.org/freefont/ // It is released under the GNU GPL main_font = SDLTTF.openFont("FreeSans.ttf",20); // Load an image for the small explosion SDLSurface small_explosion = SDLVideo.loadBMP("small_explosion.bmp"); // We will use colour keying for transparency: // all we need to do is set the colour that we will use as transparent // here, we are just using black: small_explosion.setColorKey(SDLVideo.SDL_SRCCOLORKEY,small_explosion.mapRGB(0,0,0)); // Call the explosion init, sending this image ss_explosion.init(small_explosion); // Again, load an image SDLSurface my_ship_image = SDLVideo.loadBMP("my_ship.bmp"); // Set black as the transparent colour: my_ship_image.setColorKey(SDLVideo.SDL_SRCCOLORKEY,my_ship_image.mapRGB(0,0,0)); // Again, load an image SDLSurface my_bullet_image = SDLVideo.loadBMP("my_bullet.bmp"); // Set black as the transparent colour: my_bullet_image.setColorKey(SDLVideo.SDL_SRCCOLORKEY,my_bullet_image.mapRGB(0,0,0)); // Again, load an image enemy_image = SDLVideo.loadBMP("enemy.bmp"); // Set black as the transparent colour: enemy_image.setColorKey(SDLVideo.SDL_SRCCOLORKEY,enemy_image.mapRGB(0,0,0)); // Again, load an image enemy_bullet_image = SDLVideo.loadBMP("enemy_bullet.bmp"); // Set black as the transparent colour: (you just can't beat good old cut and pasted comments) enemy_bullet_image.setColorKey(SDLVideo.SDL_SRCCOLORKEY,enemy_bullet_image.mapRGB(0,0,0)); // Create our space ship my_ship = new spaceship(my_ship_image, // The image to use for the ship new c_vector(320,240), // The starting position of the ship 300.0f, // The speed of the ship new c_vector(0,-1), // The direction that bullets are fired at my_bullet_image, // The image to use for the bullets 500.0f); // The speed of the bullets // We want some means of storing a dynamic number of space ships. // We will need to peform various operations on them, such as updating and drawing them. // They will also need to be able to shoot at us, and we want to be able to shoot at them. // Thus, we also need a way of storing bullets, and a means of removing them from the game. // We will address both these points by considering both a ship and a bullet as a 'game object'. // If we then store these game objects in a list, we can cycle through the list to update and draw them. // If something gets created, it will be added to the list, and if it gets destroyed, it will be removed from the list. // // But why not put bullets in a list for each space ship? // If we did that, we would have a problem: what if the space ship gets blown up and is deleted? // The space ship's bullet list would also be deleted, and its active bullets removed (i.e. bullets that were fired by it, and are still flying through space). // We don't want this, so the bullets need to be stored outside the ship. // Create our main game object list game_objects = new LinkedList<game_object>(); // Add the space ship to the list game_objects.add((game_object)my_ship); // We will use this for storing the direction that the user input indicates the ship should move in ship_direction = new c_vector(); // Timing is one of the most crucial aspects of an interactive game-like program. // Since the graphics on screen can take a variable amount of time to draw, // we cannot rely on a constant frame rate. Besides, if we run our program on a // faster or slower machine, it will be different again. // We must therefore calculate the amount of time that has elapsed between each // iteration of our game loop, and use this when we update things. long previous_time = 0, current_time = 0, deltaT = 0; float deltaT_s = 0.0f; current_time = System.currentTimeMillis(); previous_time = current_time; // This will store whether or not the user has asked to quit boolean quit = false; // This is our main "game loop", and we will run it // while we've not been asked to quit... while( quit == false ) { // When the user presses a key, or uses the mouse, that input is stored in SDL as an 'event'. // As an interactive program, we need to look at these events and decide what to do about them. // Our function here also returns true if we want to quit: quit = handle_events(); // In an interactive program, be it a game or whatever, we usually have a variety of things to update each frame. // These could be character animations, physics simulations, explosions, whatever we want... update(deltaT_s); // Finally, we need to draw our scene draw(); // Update our current time current_time = System.currentTimeMillis(); // Work out the frame rate for that frame deltaT_s = (float)(current_time-previous_time)/1000.0f; // For the next frame, our current time will the the previous one previous_time = current_time; } exit(0); } catch(SDLException e) // Catch SDL problems { // We shall handle any problems by printing a stack trace: e.printStackTrace(); // and an explanation if that wasn't enough System.err.println("SDL encountered a problem"); exit(1); } } }