Previous Tutorial
Next Tutorial
Posted on: 27-10-2012

Gravity Platformer Tutorial #3 : Our Character

With the look and feel defined, we can specify this a bit by adding the background story, or the main drive of the game.

The story of our game is going to be a fairly simple one. You play an alien that has crashed on an unknown planet. During our descent onto the planet our spaceship lost a lot of parts and is no longer functioning properly. To repair our shuttle enough to leave the planet, we have to collect scrap metal parts that have been scattered around the planet.

It may not be much, but your imagination will fill in the blanks, now it is time to meet our character:

I admit, he might look a bit more like a dinosaur than an alien, but I am no graphic artist so I'm quite happy with how he turned out.
If you look closely you can see some kind of belt, this is the gravity belt (which he can use to well, change the gravity) that he brought along from his homeworld.

Time to put him in our game:

  1. First we are going to create an extra package called "game.character", this will be our package for all our game characters.

  2. In this package we will create two additional classes, a Character class and a Player class.

    1. package game.character;
    2.  
    3. import org.newdawn.slick.Image;
    4. import org.newdawn.slick.SlickException;
    5.  
    6. public abstract class Character {
    7.  
    8. protected float x;
    9. protected float y;
    10. protected Image sprite;
    11.  
    12. public Character(float x, float y) throws SlickException{
    13. this.x = x;
    14. this.y = y;
    15. //in case we forget to set the image, we don't want the game to crash, but it still has to be obvious that something was forgotten
    16. sprite = new Image("data/img/placeholder_sprite.png");
    17. }
    18.  
    19. public float getX(){
    20. return x;
    21. }
    22.  
    23. public float getY(){
    24. return y;
    25. }
    26.  
    27. public void render(){
    28. sprite.draw(x-2,y-2);
    29. }
    30.  
    31. }
    Character will be our abstract class, all characters which includes our player will be extended from this class. This will be useful for rendering, and most characters will probably have a lot in common. In case you don't know the "protected" keyword, it simply means that it is only directly accessible by other classes in the same package, our Player class for example.

    You might be wondering why I subtract 2 from the Y and X coordinates on line 28, this is because I made the sprite a 36 by 36 one, surrounding the 32x32 sprite by a 2 pixel transparant border. During the testing of this tutorial I had some weird lines being drawn above the sprite, this is the result of our scaling. The extra border around the sprites alleviates this problem, but we don't really want to have both 36 by 36 sprites in the game and 32 by 32 sprites. To fix that problem we just offset the sprite a bit when we render it.

    Also, in case you are wondering, the placeholder_sprite.png can be anything you want, here is mine:
    (very original don't you think?)

  3. Next up is the Player class:

    1. package game.character;
    2.  
    3. import org.newdawn.slick.Image;
    4. import org.newdawn.slick.SlickException;
    5.  
    6. public class Player extends Character {
    7.  
    8. public Player(float x, float y) throws SlickException{
    9. super(x,y);
    10. sprite = new Image("data/img/characters/player/player.png");
    11. }
    12. }

    All we have to do is say that a Player is a Character.

  4. Now that we have those two classes we have to modify our level to support characters. We can do this by adding a list of characters to our Level class, we also need functionality to add new characters to the map and last but not least we have to be able to draw them.

    Those three things add up to the following outcome of the Level class:

    1. public class Level {
    2.  
    3. private TiledMap map;
    4.  
    5. //a list of all characters present somewhere on this map
    6. private ArrayList<Character> characters;
    7.  
    8. public Level(String level) throws SlickException{
    9. map = new TiledMap("data/levels/" + level + ".tmx","data/img");
    10. characters = new ArrayList<Character>();
    11. }
    12.  
    13. public void addCharacter(Character c){
    14. characters.add(c);
    15. }
    16.  
    17. public void render(){
    18. //render the map first
    19. map.render(0, 0, 0, 0, 32, 18);
    20.  
    21. //and then render the characters on top of the map
    22. for(Character c : characters){
    23. c.render();
    24. }
    25. }
    26.  
    27. }

    And there we go, we have a level that supports characters, which includes our player.

  5. Now all we have to do is add the player to our map. We have to keep in mind though, that our player is a special character, he carries over from map to map, so we might want to keep a reference in our LevelState. We only have to add a little bit to the init method and

    1. public class LevelState extends BasicGameState {
    2.  
    3. private Player player;
    4.  
    5. public void init(GameContainer container, StateBasedGame sbg) throws SlickException {
    6.  
    7. //once we initialize our level, we want to load the right level
    8. level = new Level(startinglevel);
    9.  
    10. //at the start of the game we don't have a player yet
    11. player = new Player(128,416);
    12. level.addCharacter(player);
    13. }
    14.  
    15. }

    If we start up the game now, we should see our character somewhere on the map.

We have our character in our world, but we can't move it yet, lets get some basic moving around going.

  1. Again we are going to create an additional package called "game.controller". In this package we will create again an abstract class and one extending from it.

  2. Our abstract class is called PlayerController and will look like this:

    1. package game.controller;
    2.  
    3. import game.character.Player;
    4.  
    5. import org.newdawn.slick.Input;
    6.  
    7. public abstract class PlayerController {
    8.  
    9. protected Player player;
    10.  
    11. public PlayerController(Player player){
    12. this.player = player;
    13. }
    14.  
    15. public abstract void handleInput(Input i, int delta);
    16.  
    17. }

    The reason why we have this abstract class is because maybe we want our game to be playing by keyboard and mouse and also with a external controller (xbox for example). All we would have to do is create an additional class that extends from this one and implement the handleInput(Input i, int delta) method.

  3. Because we want to keep it simple for now, we are going to implement a MouseAndKeyBoardPlayerController. It might be a long name, but it sure describes what it does :).

    1. package game.controller;
    2.  
    3. import game.character.Player;
    4.  
    5. import org.newdawn.slick.Input;
    6.  
    7. public class MouseAndKeyBoardPlayerController extends PlayerController {
    8.  
    9. public MouseAndKeyBoardPlayerController(Player player) {
    10. super(player);
    11. }
    12.  
    13. public void handleInput(Input i, int delta) {
    14. //handle any input from the keyboard
    15. handleKeyboardInput(i,delta);
    16. }
    17.  
    18. private void handleKeyboardInput(Input i, int delta){
    19. //we can both use the WASD or arrow keys to move around, obviously we can't move both left and right simultaneously
    20. if(i.isKeyDown(Input.KEY_A) || i.isKeyDown(Input.KEY_LEFT)){
    21. player.moveLeft(delta);
    22. }else if(i.isKeyDown(Input.KEY_D) || i.isKeyDown(Input.KEY_RIGHT)){
    23. player.moveRight(delta);
    24. }
    25. }
    26.  
    27. }

    Currently there is only a handleKyBoardInput method, but it is fairly easy to add an additional one for our mouse. But what we don't need, we don't implement. It is important to not get sidetracked, it is alright to keep in mind that we might want to add mouse functionality later but we don't want to implement it yet.

    You might see that there are a few error at lines 21 and 23, Player does not have these methods yet. So lets implement them

    1. public void moveLeft(int delta){
    2. x = x - (0.15f*delta);
    3. }
    4.  
    5. public void moveRight(int delta){
    6. x = x + (0.15f*delta);
    7. }

    You might have been wondering, where is this delta thing for? Well here you go, it is to calculate the distance we need to travel. The delta is the time in milliseconds that have passed between 2 frames. So lets say we have around 60 frames per second, this will mean our delta will be 1000/60 = 16.67. This will be rounded to 17 but Slick2D makes sure that there will be some rounded to 17 and some rounded to 16 so that it adds up to 1000 in one second. This is very important because we don't want our gameplay be affected by frame rate, someone with 50 frames per second must be able to play the game at the same speed as someone with 60 frames per second.

  4. Now all we have to do is actually tell the controller to handle the input the game gets. Again we want a reference in our LevelState, create a MouseAndKeyBoardPlayerController when we initialize our LevelState and every update cycle in the game we want our controller to handle any input that we provided.

    1. public class LevelState extends BasicGameState {
    2.  
    3. private Level level;
    4. private String startinglevel;
    5. private Player player;
    6. private PlayerController playerController;
    7.  
    8. public LevelState(String startingLevel){
    9. this.startinglevel = startingLevel;
    10. }
    11.  
    12. public void init(GameContainer container, StateBasedGame sbg) throws SlickException {
    13.  
    14. //once we initialize our level, we want to load the right level
    15. level = new Level(startinglevel);
    16.  
    17. //at the start of the game we don't have a player yet
    18. player = new Player(128,416);
    19. level.addCharacter(player);
    20.  
    21. //and we create a controller, for now we use the MouseAndKeyBoardPlayerController
    22. playerController = new MouseAndKeyBoardPlayerController(player);
    23. }
    24.  
    25. public void update(GameContainer container, StateBasedGame sbg, int delta) throws SlickException {
    26. //every update we have to handle the input from the player
    27. playerController.handleInput(container.getInput(), delta);
    28. }
    29.  
    30. public void render(GameContainer container, StateBasedGame sbg, Graphics g) throws SlickException {
    31. g.scale(Game.SCALE, Game.SCALE);
    32. //render the level
    33. level.render();
    34. }
    35.  
    36. //this method is overriden from basicgamestate and will trigger once you press any key on your keyboard
    37. public void keyPressed(int key, char code){
    38. //if the key is escape, close our application
    39. if(key == Input.KEY_ESCAPE){
    40. System.exit(0);
    41. }
    42. }
    43.  
    44. public int getID() {
    45. //this is the id for changing states
    46. return 0;
    47. }
    48.  
    49. }

And there we go, we can move our character around!

End Result

Closing Notes

I hope that you have learned something from this, if some thing were not entirely clear or if you have some questions feel free to ask them in the comments section below.

Next time we will looking into how we animate our character.

Categories: Game Design, Game Development, Java, Tutorial

Comments

Nekotripp said: (29-10-2012)
Hey man, please keep this up! This is absolutely perfect for what I'm currently trying to learn! Please keep updating this and you'll have a loyal reader here!
Beyond Louie's said: (11-11-2012)
Nice job! I'm moving on the the next one.
mikey said: (01-12-2012)
Could you go into more depth about

x = x - (0.15f*delta);

and how this moves us left, for a total beginner? Specifically, what is that 0.15f?
Frums said: (02-12-2012)
What that line does is it takes the X position of the player, then substracts how much you want to move. Because of the substraction it moves you left, if you would add it instead you would move right.

This is because the zero x and zero y are located in the top left of our screen. So lowering an X value would move us closer to the left, and increasing it would move us farther to the right.

As for the 0.15f, this is a float value. It is in a way the same as 0.15, but because java has both double and float values, you indicate that it is a float value by putting an "f" behind it.

If you would change the value of that number around you will see that by decreasing it, the character will move more slowly and by increasing it, it will move faster.
mikey said: (06-12-2012)
when you were talking about delta, where does the number 1000 come from?
Frums said: (06-12-2012)
1000 is the amount of milliseconds in a second.
So if you divide 1000 by 60, you have ~16.6667 milliseconds for every frame.
Basicly, the delta in the update is how much time in milliseconds has passed since the last update call.
Jack said: (13-03-2013)
Help! I'm getting an error. It's at Level.java and LevelState.java:

Level.java
c.render();
The method render() is undefined for the type Character

and LevelState.java
level.addCharacter(player);
The method addCharacter(Character) in the type Level is not applicable for the arguments (Player)

I know you're busy making awesome tutorials, so thanks!
Chance said: (10-04-2013)
I had the same problem. I think you need to specifically import game.character.Character in your level class, or it uses a different Character class from the Java api...



What is the name of the website? (to counter the spam)