Android SpriteWalker Demo

Introduction

I've had a long-standing goal to build an RPG someday. The biggest problem standing in the way is all my plot ideas have either been done before, or are terrible, or both. That issue has kept me procrastinating on it for about a decade now. So on a random whim I decided to take a stab at programming some of the basic mechanics and hope that a decent story idea magically appears along the way.

What I wanted to start with was a demo that accomplishes the following:

I spent some time trying to figure out what platform to do this on, I even considered the NES or Genesis for a while, and finally landed on Android since it's easy enough to work with.

Yes, I'm aware there's a perfectly good tool for developing RPGs called RPG Maker. I didn't use it because (a) I run Linux and (b) I've already established I don't have a good story idea and it wouldn't help with that.

After some trial and error the final product looks like this:

Final demo

You'll notice the uncanny resemblance to Phantasy Star 3. I used graphics I already had handy for this demo since I wanted to focus on getting the mechanics working. Whatever I decide to do with this in the future will have a new look.

I'm not a fan of reinventing the wheel. I wanted to code as little from scratch as possible. This tutorial looks at how to take pieces of code that are already available and glue them into a working demo.


Getting started

My starting point for this demo was this tutorial: [web archive link]. It covers the basics of setting-up a SurfaceView and animating a sprite. It does not cover controlling a sprite through user input, creating a map, playing music, or saving the game state. This tutorial will focus more on those aspects. If you want to learn how to setup a SurfaceView and move a sprite around then start with that link.

At the end of that tutorial we have:

So we have a sprite that can walk around, I think the first thing we need is some scenery...


Creating the map

In this demo I decided to create a 240x320 map with "tiles" that are either 16x16 or 8x8. The reason for these dimensions is that it matches the Sega Genesis (albeit flipped on its side). The long-term plan I have is to make something in the style of a Genesis RPG. I have even more delusional fantasies about developing a game for the Genesis or Sega CD which I'll save for another time. Anyway, let's create some images for the map. There are three layers in this demo: background, sprite, foreground (drawn in that order). Here are the foreground and background images:

Town background

Town background

Town foreground

Town foreground

Town foreground - debug version

Town foreground - debug version

The third image was created to assist with debugging so I could see which tiles a sprite is occupying. Unlike an actual Genesis game I'm not rendering individual tiles. Instead I'm mapping this image to a vector of map data:


MapData.java
public final int[][] townDemo={
  {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0},
  {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0},
  {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0},
  {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0},
  {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0},
  {0,0,0,0,0,0,1,1,1,1,1,1,0,0,0},
  {0,0,0,0,0,0,1,1,1,1,1,1,0,0,0},
  {0,0,0,0,0,0,1,1,1,1,1,1,0,0,0},
  {0,0,0,0,0,0,1,1,1,1,1,1,0,0,0},
  {0,0,0,0,0,0,1,1,1,1,1,1,0,0,0},
  {0,0,0,0,0,0,1,1,1,1,1,1,0,0,0},
  {0,0,0,0,0,0,1,1,1,1,1,1,0,0,0},
  {0,0,0,0,0,0,1,1,1,1,1,1,0,0,0},
  {0,0,0,0,0,0,1,1,2,2,1,1,0,0,0},
  {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0},
  {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0},
  {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0},
  {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0},
  {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0},
  {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}
  };

I went with a very simple approach of 0=free, 1=solid, 2=door (which is for a future iteration of this demo).

Lacking actual structs, I stored the map data in a simple class that resembles a struct, no apologies for offending Java purists:


GameMap.java
public class GameMap{
  private final static int DEFAULT_WIDTH=240;
  private final static int DEFAULT_HEIGHT=320;
  private final static int DEFAULT_TILE_WIDTH=16;
  private final static int DEFAULT_TILE_HEIGHT=16;
  protected int width=DEFAULT_WIDTH;
  protected int height=DEFAULT_HEIGHT;
  protected int tileWidth=DEFAULT_TILE_WIDTH;
  protected int tileHeight=DEFAULT_TILE_HEIGHT;
  protected BitmapDrawable background;
  protected BitmapDrawable foreground;
  protected int[][] data;
  public GameMap(BitmapDrawable background,BitmapDrawable foreground,int[][] data){
    this.background=background;
    this.foreground=foreground;
    this.data=data;
  }
}

Displaying a game in 240x320 on an Android device would look terrible, except maybe on one of these newfangled Android watches, so I added scaling to the GameView class. Whenever the SurfaceView is created or changed we need to calculate the scaling ratios.


GameView.java
public class GameView extends SurfaceView{
[...]
  protected float scaleX;
  protected float scaleY;
[...]
  public void surfaceCreated(SurfaceHolder holder){
    surfaceBounds=holder.getSurfaceFrame();
    map.background.setBounds(surfaceBounds);
    map.foreground.setBounds(surfaceBounds);
    scaleX=surfaceBounds.width()/(float)map.width;
    scaleY=surfaceBounds.height()/(float)map.height;
[...]
  public void surfaceChanged(SurfaceHolder holder,int format,int width,int height){
    surfaceBounds=holder.getSurfaceFrame();
    map.background.setBounds(surfaceBounds);
    map.foreground.setBounds(surfaceBounds);
    scaleX=surfaceBounds.width()/(float)map.width;
    scaleY=surfaceBounds.height()/(float)map.height;

This also meant scaling the sprite when it's drawn:


Sprite.java
[...]
  public void onDraw(Canvas canvas){
    this.update();
    if(this.moving){
      this.spriteMapX=this.currentFrame*this.frameWidth;
      this.spriteMapY=this.animationRow*this.frameHeight;
      this.sourceRect.set(
        this.spriteMapX,
        this.spriteMapY,
        this.spriteMapX+this.frameWidth,
        this.spriteMapY+this.frameHeight);
      this.destinationRect.set(
        this.x,
        this.y,
        (int)(this.x+(this.frameWidth*this.gameView.scaleX)),
        (int)(this.y+(this.frameHeight*this.gameView.scaleY)));
    }
    canvas.drawBitmap(this.spriteMap,this.sourceRect,this.destinationRect,null);
  }

In case you're wondering why I added scaling logic instead of letting the device auto-scale it's because I found that behavior inconsistent. Everything in the demo was tested on an Android 4.1 virtual machine and on a phone running 4.4. In neither case did the scaling act as expected. On one the sprite was huge and tiny on the other. The method I used produced the same results on both.


Sprite motion & touch controls

Out of that first tutorial we have a sprite that moves around but isn't controlled by the player. What I decided to implement was something similar to a virtual d-pad minus the overlay. I've played a few old-school RPGs on Android and didn't care for the permanent fixed overlays. One game that I thought handled the virtual controls well was Final Fantasy V because you could swipe and hold anywhere on the screen to move your party around. I copied that approach minus the overlay that appears.

Before we do that, here are the sprite sheets for this demo. There's a transparent version and debug version with blue background to help test the collision detection later on.

Sprite sheet

Sprite sheet

Sprite sheet - debug version

Sprite sheet - debug version

First let's define the directions a sprite can move. We're starting with four because those are the animations we have, adding support for eight directions wouldn't be difficult.


public enum Direction{up,left,down,right};

Now we need to detect which direction the player swiped and move the sprite as long as the user is continuing to press the screen. The player doesn't need to swipe the screen the entire time, if they swipe to the right and keep pressing the sprite should continue moving. Once the player stops touching the screen the sprite should stop moving. The VelocityTracker class is especially helpful here.


GameView.java
[...]
  protected VelocityTracker mVelocityTracker=null;
[...]
  @Override
  public boolean onTouchEvent(MotionEvent event){
    int actionId=event.getAction();
    if(actionId==MotionEvent.ACTION_DOWN){
      if(mVelocityTracker==null){
        mVelocityTracker=VelocityTracker.obtain();
      }else{
        mVelocityTracker.clear();
      }
      mVelocityTracker.addMovement(event);
    }else if(actionId==MotionEvent.ACTION_UP){
      this.playerSprite.stopMoving();
    }else if(actionId==MotionEvent.ACTION_MOVE){
      int index=event.getActionIndex();
      int pointerId=event.getPointerId(index);
      mVelocityTracker.addMovement(event);
      mVelocityTracker.computeCurrentVelocity(1000);
      float vx=VelocityTrackerCompat.getXVelocity(mVelocityTracker,pointerId);
      float vy=VelocityTrackerCompat.getYVelocity(mVelocityTracker,pointerId);
      float vxa=Math.abs(vx);
      float vya=Math.abs(vy);
      if((vx==0.0f)&&(vy==0.0f)){
        //TODO - this is where we want to handle clicking on an object
      }else if((vxa<100)&&(vya<100)){//prevent the sprite from moving on very small swipes
        //small movement, ignore
      }else if(vxa>vya){//moving left/right
        if(vx<0.0f){
          this.playerSprite.move(Direction.left);
        }else{
          this.playerSprite.move(Direction.right);
        }
      }else{//moving up/down
        if(vy<0.0f){
          this.playerSprite.move(Direction.up);
        }else{
          this.playerSprite.move(Direction.down);
        }
      }
    }
    return(true);
  }
So this starts the sprite walking, I suppose we better make it stop before it walks through a wall...

Collision detection

We need to set the collision zone for the sprite. This should not be the entire bounding box for the sprite but only the lower half. This allows the sprite to walk alongside the bottom edge of walls or other sprites. We do that by defining a Rect that represents the collision area:


Sprite.java
[...]
  private Rect collisionRect=new Rect(0,0,0,0);
  public Rect getCollisionRect(){
    this.collisionRect.bottom=this.destinationRect.bottom;
    this.collisionRect.left=this.destinationRect.left;
    this.collisionRect.right=this.destinationRect.right;
    this.collisionRect.top=((this.destinationRect.bottom+this.destinationRect.top)/2);
    return(this.collisionRect);
  }

We next need to be able to map a set of surface coordinates to the map coordinates:


GameView.java
[...]
  private int toCY(float surfaceY){
    float scaled=surfaceY/this.scaleY;
    int cy=(int)Math.floor(scaled/(float)this.map.tileHeight);
    return(cy);
  }
  private int toCX(float surfaceX){
    float scaled=surfaceX/this.scaleX;
    int cx=(int)Math.floor(scaled/(float)this.map.tileWidth);
    return(cx);
  }

So now before a sprite moves we can test if it's about to hit something:


GameView.java
[...]
  private boolean hitTest(Sprite sprite){
    if(sprite.isMoving()){
      Rect cRect=sprite.getCollisionRect();
      switch(sprite.direction){
        case up:{
          int cyt=this.toCY(cRect.top);
          //test if near the top edge
          if(cyt==0){
            Rect spriteRect=sprite.getRect();
            //sprite should be able to reach the top edge
            if((spriteRect.top-sprite.yStep)>0){
              return(false);
            }
            return(true);
          }else{
            int sprite_cxl=this.toCX(cRect.left);
            int sprite_cxr=this.toCX(cRect.right);
            if((this.map.data[cyt-1][sprite_cxl]==0)&&(this.map.data[cyt-1][sprite_cxr]==0)){
              return(false);
            }else{
              //sprite should be able to reach the bottom edge of this tile
              int bottomEdge=(int)(cyt*this.tileHeightScaled);
              if((cRect.top-sprite.yStep)>bottomEdge){
                return(false);
            }
            return(true);
          }
        }
      }
[... and so on for the other three directions ...]

Now the player can move the sprite around the town and bump into walls. It still feels a little sterile so let's spruce things up with a little music...


Playing music

I'm still not a fan or reinventing the wheel so most of the audio code is from https://github.com/alexsilva/MusicService and https://developer.android.com/training/managing-audio/index.html. I made a couple additions to MusicService to make it easier to change songs once it's started.


MusicService.java
[...]
  private int resourceId=-1;
  public void setResourceId(int resourceId){this.resourceId=resourceId;}
  public int getResourceId(){return(this.resourceId);}
[...]
  public void create(){
    [...]
    mPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
    [...]
    }
[...]
  public void start(int resourceId){
    this.setResourceId(resourceId);
    this.start();
  }

I also added code to the activity to pause & to change the track...


MainActivity.java
public class MainActivity extends Activity implements ServiceConnection{
[...]
  private boolean musicServiceIsBound=false;
  private MusicService musicService;
  [...]
  protected void onCreate(Bundle savedInstanceState){
  [...]
    //start the music service
    this.setVolumeControlStream(AudioManager.STREAM_MUSIC);
    Intent music=new Intent(this,MusicService.class);
    this.startService(music);
    this.doBindService();
    [...]
  }
[...]
  public void setBGMusic(int resourceId){
    if((this.musicServiceIsBound)&&(this.musicService!=null)){
      this.gameState.bgMusicResourceId=resourceId;
      if(resourceId!=this.musicService.getResourceId()){
        this.musicService.stop();
        this.musicService.start(resourceId);
      }
    }
  }

You may have noticed a reference to something that looks like a game state in that code snippet. That brings us to the last part of this demo...


Saving the game state

When the application is paused we want to save the state of the game and restore it on create or resume. This will also be the foundation for later adding save game support. We'll start with a really simple class, again acting like a struct, to hold things we want to restore.


GameState.java
public class GameState implements Serializable{
  private static final long serialVersionUID=1L;
  protected final static int VERSION=(int)serialVersionUID;
  protected int bgMusicResourceId=-1;
  protected int playerSpriteX=0;
  protected int playerSpriteY=0;
  protected Direction playerSpriteDirection=null;
  protected GameState(){ }
}

After trying a couple different techniques I found the easiest approach is to dump the game state into a JSON string and save it as preference.


MainActivity.java
  [...]
  private final static String PREF_KEY_SAVE_STATE="savestate"+GameState.VERSION;
  private GameView gameView;
  private Gson gson=new Gson();
[...]
  private void loadGameState(){
    String saveStateJson=this.getSharedPreferences(PREF_NAME,MODE_PRIVATE).getString(PREF_KEY_SAVE_STATE,"");
    if(saveStateJson.length()>0){
      this.gameState=this.gson.fromJson(saveStateJson,GameState.class);
    }
  }
  [...]
  protected void onResume(){
    [...]
    this.loadGameState();
    if(this.gameState.bgMusicResourceId>0){
      this.setBGMusic(this.gameState.bgMusicResourceId);
    }
    if(this.gameView!=null){
      this.gameView.gameState=this.gameState;
    }
    [...]
  }
  [...]
  protected void onPause(){
    [...]
    String saveStateJson=gson.toJson(this.gameState);
    SharedPreferences prefs=this.getSharedPreferences(PREF_NAME,MODE_PRIVATE);
    Editor editor=prefs.edit();
    editor.putString(PREF_KEY_SAVE_STATE,saveStateJson).apply();
    [...]
  }

When the SurfaceView is created we'll need to move the sprite back to its previous location. And finally we'll save the sprite location after every step. Alternatively I could just grab this during onPause, maybe I'll go change that after I upload this page.


GameView.java
[...]
  protected GameState gameState=null;
[...]
  public void surfaceCreated(SurfaceHolder holder){
[...]
    if(gameState!=null){
      playerSprite.moveTo(gameState.playerSpriteX,gameState.playerSpriteY,gameState.playerSpriteDirection);
    }
    [...]
  }
[...]
  protected void onDraw(Canvas canvas){
    [...]
    this.playerSprite.onDraw(canvas);
    this.map.foreground.draw(canvas);
    this.gameState.playerSpriteX=this.playerSprite.x;
    this.gameState.playerSpriteY=this.playerSprite.y;
    this.gameState.playerSpriteDirection=this.playerSprite.direction;
    [...]
  }


Related