Making a game with Flash Develop + Flixel: Part 6, Cameras and Text

Previously:

(Want to bypass the long explanations and see the final product? Here, download this post’s code as a ZIP file.)

Before we can jump into code for this post, we need to cover what exactly a camera is. Instead of having viewports or even subsections of the screen, Flixel has FlxCameras. These cover different areas and can be set to follow the movements of sprites or other objects.

By default, Flixel creates one camera and centers it on the running state and its objects. Whatever dimensions were set in the FlxGame object, those are the same as the camera. Move outside those bounds and the sprite essentially disappears from view.

Changing the values for the default camera, then, allows us to move as a sprites is moving. Within a larger map, the camera will stay centered and the world will appear to scroll as a sprite moves within it.

However, before we change the values, we need to know two things:

  1. The total dimensions of the “world”
  2. Which object to follow

We also need to set all this up before a FlxState is running, otherwise it will use the default values by, well, default.

In order to do all that, we need know something important about Flixel (and ActionScript 3): create does the making, not a constructor. In other words, the very first thing called is a “create()”. To have anything set or run before a object officially starts, it needs to be within that function.

And, since a FlxState will already have a create function, we need to override it just like we have for update.

package
{
/**
* ...
* @author Dan Cox
*/
import org.flixel.*;
public class PlayState extends FlxState
{
private var player:PlayerSprite;
private var map:MapTilemap;
public function PlayState()
{
}
override public function create():void
{
map = new MapTilemap();
add(map);
player = new PlayerSprite(50,50);
add(player);
}
override public function update():void
{
super.update();
FlxG.collide(player, map);
}
}
}
view raw PlayState.as hosted with ❤ by GitHub

Now that we have the create function overridden, we can update the default camera by changing the world bounds to match the size of the map, setting the camera bounds to match that, instructing the camera to follow the player, and then updating the player object’s own list of cameras.

package
{
/**
* ...
* @author Dan Cox
*/
import org.flixel.*;
public class PlayState extends FlxState
{
private var player:PlayerSprite;
private var map:MapTilemap;
public function PlayState()
{
}
override public function create():void
{
map = new MapTilemap();
add(map);
player = new PlayerSprite(50,50);
add(player);
FlxG.worldBounds = new FlxRect(0, 0, map.width, map.height);
FlxG.camera.setBounds(0, 0, map.width, map.height, true);
FlxG.camera.follow(player);
player.cameras = new Array(FlxG.camera);
}
override public function update():void
{
super.update();
FlxG.collide(player, map);
}
}
}
view raw PlayState.as hosted with ❤ by GitHub

Compiling and running the game won’t show any real change.

step1

We are still moving around within the tilemap setup several parts ago with the same green character from the last part.

However, what we have changed, even if the game doesn’t show it yet, is the ability to have a very large world instead of the 340 x 240 established back in Part 2.

To do that, all we need to change is the “map1.txt” file. Because MapTilemap reads that and, in turn, the size of the game world is based on its dimensions, we can expand the world by editing only one file.

Before we do that, let’s expand out tile set and then add in our new tiles with the edit to “map1.txt”

(Note: I’m pulling from my previous guides on drawing ground, wall, building, doors, and plant tiles.)

step2
Click this image for the actual tilemap.

Using our new tiles, we can make a much larger map. (Download or copy-and-paste from this file.)

Which translates into a greater world to explore.

step3

(Note: We can’t actually use the door tiles as actual doors yet. Right now, they are merely part of the background.)

With our (brave) new world, we also want the ability to give visual feedback to the player in the form of text. These would be descriptions or even dialogue shown as the result of an interaction.

What we want then is FlxText. This will allow us to show text on the screen.

However, there is a problem. FlxText is designed to show text, nothing else. If we want a background and the text, and the ability to press a key to progress through messages, we need something more advanced.

For that, we can use an extended FlxGroup composed of two FxSprites and a FlxText: FlxDialog!

(Note: A FlxGroup is a collector object. It can hold any other FlxObjects, including other FlxGroups. It’s like an array or other data structure, but designed with extras like updating and drawing its members as well.)

package
{
import org.flixel.*;
public class FlxDialog extends FlxGroup
{
/**
* Use this to tell if dialog is showing on the screen or not.
*/
public var showing:Boolean;
/**
* The text field used to display the text
*/
private var _field:FlxText;
/**
* Called when dialog is finished (optional)
*/
private var _finishCallback:Function;
/**
* Stores all of the text to be displayed. Each "page" is a string in the array
*/
private var _dialogArray:Array;
/**
* Background rect for the text
*/
private var _bg:FlxSprite;
private var _bgc:FlxSprite;
internal var _pageIndex:int;
internal var _displaying:Boolean;
internal var _endPage:Boolean;
internal var _key:String;
/**
* The FlxDialog Constructor
* @param X
* @param Y
* @param Width
* @param Height
* @param displaySpeed
* @param backgroundColor
* @param outlineColor
*/
public function FlxDialog(X:Number = 0, Y:Number = 0, Width:Number = 310, Height:Number = 72, backgroundColor:uint = 0xFF000000, outlineColor:uint = 0xFF3765FF, outlineThickness:uint = 2)
{
_bg = new FlxSprite(X, Y).makeGraphic(Width, Height, outlineColor);
_bgc = new FlxSprite(X + outlineThickness, Y + outlineThickness).makeGraphic(Width - outlineThickness * 2, Height - outlineThickness * 2, backgroundColor);
_bg.scrollFactor.x = _bg.scrollFactor.y = _bgc.scrollFactor.x = _bgc.scrollFactor.y = 0;
add(_bg);
add(_bgc);
_field = new FlxText(_bgc.x + outlineThickness, _bgc.y + outlineThickness, Width, "");
_field.scrollFactor.x = _field.scrollFactor.y = 0;
add(_field);
_bg.alpha = _bgc.alpha = 0;
_key = "SPACE";
}
/**
* Change the key to press to progress the dialog
* @param key
*/
public function setKey(key:String):void
{
_key = key;
}
/**
* Set the formatting of the FlxText internal object
* @param font
* @param Size
* @param Color
* @param Alignment
* @param ShadowColor
*/
public function setFormat(font:String = null, Size:Number = 8, Color:uint = 0xffffff, Alignment:String = null, ShadowColor:uint = 0):void
{
_field.setFormat(font, Size, Color, Alignment, ShadowColor);
}
/**
* Prepare to show the dialog
* @param pages
*/
public function showDialog(pages:Array):void
{
_pageIndex = 0;
_field.text = pages[0];
_dialogArray = pages;
_displaying = true;
_bg.alpha = _bgc.alpha = 1;
showing = true;
}
/**
* Update the FlxDialog object
*/
override public function update():void
{
if (_displaying)
{
_field.text = _dialogArray[_pageIndex];
}
if (FlxG.keys.justReleased(_key))
{
if (_displaying)
{
_displaying = false;
_field.text = _dialogArray[_pageIndex];
if (_pageIndex == _dialogArray.length - 1)
{
_pageIndex = 0;
_field.text = "";
_bg.alpha = _bgc.alpha = 0;
if (_finishCallback != null)
_finishCallback();
showing = false;
}
else
{
_pageIndex++;
_displaying = true;
}
}
}
super.update();
}
/**
* Set the callback function for when all pages have been displayed
*/
public function set finishCallback(val:Function):void
{
_finishCallback = val;
}
}
}
view raw FlxDialog.as hosted with ❤ by GitHub

And then, to use it, we update our PlayState with a simple interaction of pressing “Enter” and seeing a dialog box. (With pressing “Space” to dismiss it.)

package
{
/**
* ...
* @author Dan Cox
*/
import org.flixel.*;
public class PlayState extends FlxState
{
private var player:PlayerSprite;
private var map:MapTilemap;
private var dialog:FlxDialog;
public function PlayState()
{
}
override public function create():void
{
map = new MapTilemap();
add(map);
player = new PlayerSprite(50,50);
add(player);
FlxG.worldBounds = new FlxRect(0, 0, map.width, map.height);
FlxG.camera.setBounds(0, 0, map.width, map.height, true);
FlxG.camera.follow(player);
player.cameras = new Array(FlxG.camera);
dialog = new FlxDialog(0, 160, FlxG.width, 80);
dialog.setFormat(null, 8, 0xFFFFFFFF);
dialog.finishCallback = removeDialogBox;
}
override public function update():void
{
super.update();
FlxG.collide(player, map);
if (FlxG.keys.ENTER)
{
var blank:Array = new Array();
blank.push("You pressed 'Enter'!");
dialog.showDialog(blank);
add(dialog);
}
}
private function removeDialogBox():void
{
remove(dialog);
}
}
}
view raw PlayState.as hosted with ❤ by GitHub

However, if you now compile and run the code, you will notice something very funny. If we press “Enter” but don’t press “Space”, the dialog box will follow the character around. We need to make sure the character stops while messages are being shown.

For that, let’s add a public Boolean value within PlayerSprite.

package
{
/**
* ...
* @author Dan Cox
*/
import org.flixel.*;
public class PlayerSprite extends FlxSprite
{
[Embed(source="data/walk.png")]
private var WalkPng:Class;
private var speed:int = 130;
public var isStopped:Boolean = false;
public function PlayerSprite(X:Number = 0, Y:Number = 0)
{
super(X, Y);
loadGraphic(WalkPng, true, false);
addAnimation("Down", [0, 1, 2], 10, false);
addAnimation("Right", [3, 4, 5], 10, false);
addAnimation("Left", [6, 7, 8], 10, false);
addAnimation("Up", [9, 10, 11], 10, false);
}
override public function update():void
{
super.update();
velocity.x = 0;
velocity.y = 0;
if (!isStopped)
{
if (FlxG.keys.W || FlxG.keys.UP)
{
velocity.y -= speed;
play("Up");
}
else if (FlxG.keys.S || FlxG.keys.DOWN)
{
velocity.y += speed;
play("Down");
}
else if (FlxG.keys.A || FlxG.keys.LEFT)
{
velocity.x -= speed;
play("Left");
}
else if (FlxG.keys.D || FlxG.keys.RIGHT)
{
velocity.x += speed;
play("Right");
}
}
}
}
}
view raw PlayerSprite.as hosted with ❤ by GitHub

And, of course, we then update out PlayState as well, adding in code to stop when starting a dialog box and un-stop the character after it has been completed.

package
{
/**
* ...
* @author Dan Cox
*/
import org.flixel.*;
public class PlayState extends FlxState
{
private var player:PlayerSprite;
private var map:MapTilemap;
private var dialog:FlxDialog;
public function PlayState()
{
}
override public function create():void
{
map = new MapTilemap();
add(map);
player = new PlayerSprite(50,50);
add(player);
FlxG.worldBounds = new FlxRect(0, 0, map.width, map.height);
FlxG.camera.setBounds(0, 0, map.width, map.height, true);
FlxG.camera.follow(player);
player.cameras = new Array(FlxG.camera);
dialog = new FlxDialog(0, 160, FlxG.width, 80);
dialog.setFormat(null, 8, 0xFFFFFFFF);
dialog.finishCallback = removeDialogBox;
}
override public function update():void
{
super.update();
FlxG.collide(player, map);
if (FlxG.keys.ENTER)
{
var blank:Array = new Array();
blank.push("You pressed 'Enter'!");
dialog.showDialog(blank);
add(dialog);
player.isStopped = true;
}
}
private function removeDialogBox():void
{
remove(dialog);
player.isStopped = false;
}
}
}
view raw PlayState.as hosted with ❤ by GitHub

Finally, we have the ability to generate and walk around in a large world. We also have the start of a dialog system where actions can generate visual feedback in the form of text.

In Part 7, we add NPCs, and begin to track what the player has interacted with in the game.