Changes V8
Added screenshake and flash when Death kills
Added conversations

This update was a minor one but it hits another milestone towards a finished product.  The plan for DeathRun is to tell a story.  I'll talk a little bit more about the actual plot and how the story will be told later, but for now I needed a way to add dialogue to the game.  After a little bit of coding and the magic of FlxSubStates I've got working conversations between the player and Death.  I also added some screenshakes and flashes when Death kills something to add to the sense of real power Death carries, but that takes two lines of code and isn't anything exciting.

Talking to Death

I am going to make the conversation take place in a FlxSubState.  SubStates are neat objects that function almost identically to FlxStates (like our LevelState) but have a couple key differences.  Normally when you switch between states everything in the state is destroyed and you have to recreate it in the new state if you want to use it.  This actually helps with things like garbage collection and reducing memory leaks, but it becomes a problem if I want to do something like pause the game.  I either have to put a bunch of special logic in my GameState to hold the updates and deal with different buttons, or switch to a PauseState create a new GameState when the game is resumed.  Either of those would get more complicated than I would like, but luckily FlxSubStates work well for this. 

SubStates are run from another State and pause that state instead of disposing of it.  Then the substate code runs.  When the substate is closed the original State picks right back up where it left off.  This is powerful stuff and will work great for my conversations.  I want the game to freeze while Death and the player converse, so simply dropping all my conversation code into a SubState and launching it from LevelState will handle all that for me.

Here's what I want to end up with.

class Converse extends FlxSubState
{

	//Display variables
	//The picture to display.
	var pic:FlxSprite;
	//The name of the character speaking
	var name:FlxText;
	//The box to display the text in.
	var box:FlxSprite;
	//The text to display.
	var t:FlxText;
	//The message to display (e.g. "Press any key to continue")
	var message:FlxText;

	//Array of singleDisplays that will be shown in order.
	var list:Array<SingleDisplay>;

Here's the start to my Converse SubState.  I'm going to need a couple of variables.  I'll need a couple of sprites and a text objects.  The pic:FlxSprite will hold the image of the character speaking.  The name:FlxText will be a simple text description of the name.  Box:FlxSprite will be the box that everything will display inside.  message:FlxText will be the actual text that should be displayed inside the box.  Lastly, the message:FlxText will say "Press Taunt to continue" for now, but might be changed later.

I am also creating an array of SingleDisplays which I haven't created yet.  A singleDisplay will be all the data needed to show a single box and I'll create multiples of these to have a back and forth dialog.  The basic plan is to queue a bunch of singleDisplays up in a row and step through them whenever the user presses the spacebar.  When the queue is empty I know that the conversation is over and it can return control back to the LevelState so the level can continue to play.  Make sense?

Importing data using JSON files

I also needed to think of a way to create these conversations.  I don't really want to sit down and write a bunch of code when I generate these, so I instead decided to write this information into a JSON file and read it out of there.  JSON files stand for Javascript Object Notation files and are text documents structured in a way that makes them easy for computers to read.  Here's what the JSON file for this conversation will look like.

[
{"name":"Death","text":"Don't you ever get tired of running away all the time?", "color":"purple"},
{"name":"Mort","text":"Not really.  It's better than the alternative."},
{"name":"Death","text":"I don't think so.","color":"purple"},
{"name":"Mort","text":"Well, you wouldn't."}
]

The [ and ] tags specify I am suplying an array of values, while the { and } specify an object.  So I have an array of 4 objects.  Each object is made up of a collection of strings and values that can be in any order.  So the strings are "name", "text", and "color", while the values are "Death", "Blah blah blah", "purple".  You'll notice that when Mort talks (because that's what I've decided to name him) he doesn't get a color value.  That's ok because I'm going to default to white in my code.

This will make it easy to add new conversations to my game.  Each time I will just point the Converse object at a new JSON file and it will take care of the rest!

Working with JSON files in Haxe is easy because they have provided a Json object to do all the heavy lifting for me. You can easily turn any object into a JSON file by calling Json.stringify(obj).  In the same way, you can parse a JSON file out by calling Json.parse().  The trick is, I need to tell my code what this JSON is going to parse into.  To do that, I'm using a typeDef.

A typeDef is simply a way to tell Haxe what it should expect data to look like.  I'm going to create a new file called SingleDisplay.hx and put the following in it:

typedef SingleDisplay =
{
	//Name of the character to speak
	var name:String;
	
	//The text that the character should speak
	var text:String;

	//The color of the text.
	var color:String;
}

This tells the compiler anything that I call a SingleDisplay is going to have a name:String, text:String, and a color:String.  Notice that this lines up with exactly what my JSON file has in it.  My JSON file is an array of SingleDisplays, which is the same thing my variable was declared as.  It is nice when everything lines up!

Creating the SubState

Let's go to the new function of my Converse SubState.

public function new(BGColor:Int=FlxColor.TRANSPARENT) 
{
	super(BGColor);
		
	pic = new FlxSprite(125,300);
	name = new FlxText(125, 260, 96,"", 16);
	box = new FlxSprite(100,250, Reg.CONV_LOC+ "ConversationBox.png");
	t = new FlxText(240,260,470,"",16);
	message = new FlxText(260,580,0,"Press Taunt to continue", 14);
	
	add(box);
	add(pic);
	add(name);
	add(t);
	add(message);
}

The super(BGColor) tells my SubState what color it should set the background to.  I want to be able to see my LevelState, so I will leave that as transparent, which is the default.  Next I create my objects in the locations that I want them to display on the screen.  Then I add them to the stage making sure that I get the order right (so the things farthest back get added first).

I also need a way to prime my conversation so it is ready to play.  I'm going to do this in an init() function that I will call from LevelState before the state is launched. 

public function init(input:String) {
	var textIn = Assets.getText(Reg.CONV_LOC + input + ".txt");
	list = Json.parse(textIn);
	if(list.length > 0)
	loadLine();
}

The first thing I need to do is get the JSON file as text so I can parse it.  I'm going to use the Assets.getText() function to do that like I have elsewhere.  The only thing special about this is I've created a variable in my Reg class that points to the root location where I'm going to store all my conversation files.

public static var CONV_LOC = "assets/data/";

Input is just the name of the conversation that I want to play.  Easy enough.

Once I have my JSON file as text I can parse it.  By assigning the result to my list variable, I'm telling Haxe that this JSON file is an array of SingleDisplays.  This will let me access it like a normal array of objects. 

list = Json.parse(textIn);

Next I'm going to load up the first line of my conversation.  I'm using a function to do this because I will end up doing it a lot and I want the code to be consistent.

if(list.length > 0)
	loadLine();

Lets look at the loadLine function:

private function loadLine() {
	//Get the first element of the list.
	var thisBox = list.shift();
	//Load the picture for the speaker
	pic.loadGraphic(Reg.CONV_LOC + thisBox.name + ".png", false);
	//Set the size
	pic.setGraphicSize(96, 0);
		
	//Set the name
	name.text = thisBox.name;
	//Load the line into the text field
	t.text = thisBox.text;
	//Check the color.
	if (thisBox.color == null)
	t.color = FlxColor.WHITE;
	else
	t.color = FlxColor.PURPLE;
}

There really isn't much going on here.  I'm just getting the element in the front of my list array and assigning values to the objects I already created.  I store this element in an array called thisBox because I suck at picking variable names.

The first thing I need to do is load a picture in my pic sprite.  I want to load my graphics from the same location that all my conversation stuff lives, so I'm going to use my Reg.CONV_LOC variable.  Then I'm going to use the name in my Json file.  So when Death speaks, it will display "assets/data/Death.png".

//Load the picture for the speaker
pic.loadGraphic(Reg.CONV_LOC + thisBox.name + ".png", false);

It should look like this:

Next I'm going to set the graphic size to be 96 pixels wide so it is consistent.  I'm going to leave the height at 0 so HaxeFlixel will keep the aspect ratio the same.

pic.setGraphicSize(96, 0);

Then I'm just going to load the rest of the variables.  The only thing not straightforward is the color field.  Right now it just looks to see if the color field is present.  If yes it will display as purple. If it is missing it will display as white.

if (thisBox.color == null)
	t.color = FlxColor.WHITE;
	else
	t.color = FlxColor.PURPLE;

Putting it all together

Now I just need to mess with the update function of my SubState so I can listen for keypresses to cycle through my list.  That is an easy thing to do:

override public function update():Void 
{
	InputHelper.updateKeys();
	
	//If we just hit the button.
	if (InputHelper.isButtonJustPressed("Taunt")) {
		//If there are no more elements in the list we are finished.
		if (list.length == 0)
		this.close();
		else 
		//Load the next line.
		loadLine();
	}
	super.update();
}	

We call the normal InputHelper.updateKeys() function then listen for the taunt button to be pressed.  When it is we look at the list.  If it is empty then the conversation is over and the form can be closed.  Otherwise we run the loadline() function again and start over.

Lets jump back to the LevelState and start the subform when the level starts.

//Declare the variable
var conv:Converse;

//Create and init a test converse object at the bottom of the create function.
conv = new Converse();
conv.init("TestLevel");

Here we are setting up the conversation to run.  TestLevel is the name of my JSON file.  Now we just need to switch to the form.  I'm going to do that in my update function.

if (conv != null) {
	//Open the substate
	this.openSubState(conv);
	//Set the substate equal to null when it is finished.
	conv = null;
}

I decided to implement it this way in case I ever want to have a conversation in the middle of a level.  This way, whenever the conv variable isn't null it will switch to and play that state.  When it is done it nulls the variable.  Just like that my conversations are done!

Adding Flashing and Screenshake

Death needs to be more impressive when he hits things.  By adding this to the deathHits function after the target.kill() call he is.

FlxG.camera.flash(FlxColor.WHITE, .2);
FlxG.camera.shake(.01,.5);

Code available here.

 

Add comment


Security code
Refresh