[Phaser 3 – WIP] Small Untitled RPG

Few days ago I have decided to start an RPG project, as that’s what I’ve always wanted to make. I will be fairly limited with graphic assets and time, but at least the coding part will be fun. I will make do with what I have available.

Right now I have depth sorting implemented:
depth_sort

I’m using Time Fantasy art assets.
Tiled for level editor.
Haxe as programming language.
Phaser 3 as framework.
Coffee as fuel.

So far I’ve went through map loading (surprisingly simple in Phaser 3), animations based on keyboard input and basic collisions using Phaser’s Arcade.

Then I switched to using Differ for collisions, because Phaser’s Arcade physics couldn’t handle custom shapes per tile by default. Given that I expect more requirements from the collisions/physics system, I like having more control over things instead of letting Phaser handle it all for me.

After that, it was time to refactor all the code, because it was getting messy. I choosed to use ECS architecture, because I used it before and I find it very nice for these kind of games. Especially considering how big the game might grow, it’s important to keep the code clean. I was deciding between using Elias Ku’s ECX library and Kevin Leung’s ECS. Ultimately it probably doesn’t matter, but I chose Kevin’s.

At this point the ECS consists of RenderSyncer, KeyboardInput, Movement, PhysicsDebugDraw, PhysicsSystem systems, and Velocity, ActorState, DifferShape, InputControllable, Position, Renderable, StaticCollidable components. This will change a lot as the work continues.

Next I made a custom preprocessor for Tiled level files. I’m converting them to custom JSON that contains only the data I need, in format the game expects. This also allows for making rudimentary checks against typos and missing data. It also means I’m no longer using Phaser’s tilemap loader, and instead had to write a bit of code to populate the tilemaps myself. While this is some extra work, it allows for special handling.

Last thing I did was implement depth sorting. Phaser can’t render different tiles at different depth, or allow you to insert something (like a moving character) in between tiles. If you look at the GIF on top, you see the character can move behind and in front of the tree log and the sign board. If they were part of a Phaser’s tilemap layer, it wouldn’t work. Instead, I mark the layer in Tiled as requiring depth sorting, and the game then creates individual depth sorted sprites. This way I didn’t have to change the level editing workflow and won’t need to manually handle future similar cases.

Next I plan to work on some UI, changing location (loading different maps, like insides of a building), NPCs and interaction, and combat system.

7 Likes

Looks really cool! I’ve always been interested in HAXE.

Keep us posted.

Since the last devlog there were 10 commits between 2019/01/27 and 2019/02/15, totaling 21 commits.

I started out by adding new type of objects to my Tiled map.

message_editor

A bit more messing around and I can now have basic message interaction in the game.

That included adding overlay UI system, some refactoring, support for nineslice scaled ui elements. Handling events between various ECS systems without tight coupling (Keyboard Input System sends an Interaction event that Interaction System then handles).

I also refactored how I change what animation to play on a character. Before it was explicit from keyboard input, but planning forward I changed it to be implicit from actor’s velocity. This will work with different input systems (point and click) and also for characters controlled via AI.

Then I added our first NPC. Meet Rufus.

friend

Since I wanted to play with our new friend, it was time to add some AI.

I decided to implement Behavior Trees. In the ECS spirit, they are completely stateless (the tree/logic itself doesn’t store any data, everything is kept on entities). I define them in XML (which will be improved later), and they compile down to basically an array of functions. Since they are stateless, the same tree can be used to run any number of entities.

behavior_tree

That’s it for this dev log. As for ECS changes, I’ve renamed Movement system into InputMovement, added AISystem, ActorAnimation, InteractionSystem systems. Also added AIState and Interaction components. For other parts, added UI and Overlay Phaser scenes, and MessageBoard UI element.

In the next dev log I will talk more about Behavior Tree AI and my approach to it.

1 Like

Since the last devlog there were 13 commits between 2019/02/16 and 2019/02/22, totaling 34 commits. I have worked 35 hours during this period, bringing the total to 83 hours.

This one is entirely about messing with AI, iterating and refactoring the approach.

Initial Approach

It started with basic xml Behavior Tree from the last devlog. First thing I did though, was updating Haxe to version 4 Release Candidate, in preparation to using inlined markup.

Truth is the xml in the previous devlog wasn’t really used. It was just a template for how I would want it to look. What it actually looked like in code was:

forest = [
	{ behavior: 'Sequence', child: 1 },
	{ behavior: 'Run', sibling: 2, config: { angle: 0 } },
	{ behavior: 'Run', config: { angle: Math.PI } },
];

Where each behavior name mapped to function(nodeId:Int, entity:Entity). Why a function and not some class instance? Because in the ECS spirit, the state, i.e. all the data, is supposed to be stored inside entity’s components. The behavior tree itself should be completely stateless.

So for example Sequence, which needs a state to remember which child it’s currently executing, would use the passed entity to get the AI component and read/write the state there.

But writing it out like in the code sample above is pretty tedious and very error prone. The hierarchy of the tree is mapped using child and sibling properties, which simply contain an integer index pointing to another behavior. Doing this by hand makes my brain hurt.

Macro Powered

Haxe macros to the rescue!

With a bit of magic, I can achieve the same result from the XML file at compile time. Generating the same code by simply doing forest = AIParser.parseAI('data/ai.xml'); Of course I had to write the code to actually do that.

But the behavior logic is still defined as a function, mapped to the proper behavior by a string. I don’t like strings. Too easy to make typos and they often move errors from compile time to runtime. Solution? More macros!

What I did was I turned the Behavior into a class (I know, I know, I said no state and all, but keep reading). Now every Behavior has a class name and has to extend from the Behavior base class. It also made all the child, sibling properties fully typed.

So instead of array of, essentially string names, there is now array of Behavior instances. What about the state? Well, behaviors still need configuration. The child and sibling are part of that. So having instances makes sense. All those properties are final so I can’t accidentally rewrite them. So instead of { behavior: 'Sequence', child: 1 } there’s now new Sequence(1). If the behavior has more properties, they are all added in the constructor, all fully type checked.

But if there’s an error in the XML, while the macro can report it, it might not be entirely obvious what the error is, as inside XML all we have are strings.

Inlined Behavior Trees

Haxe is awesome, I hope that’s clear by now. I mentioned inlined markup support, what is it? Shortly, it allows me to write the XML right inside the Haxe source, process it via a macro and report back. That means I get syntax coloring, errors reported right there, as I write the code, and bunch of other benefits.

I have skipped a couple of iterations, but end result is basically that the behavior tree is declared directly in the code, fully typed and pretty (prettier than here, as the web highlighter doesn’t handle it correctly), like this:

public static var DoggyRun = 
	<Sequence>
		<Run angle={0} duration={1} />
		<Run angle={Math.PI} duration={1} />
	</Sequence>

And the actual behaviors are declared as classes like this:

class Sequence extends Behavior {
    function init():ChildData {
        return {
            child: this.child
        }
    }
    function execute(data) {
        while(true) {
            var child = forest[data.child];
            var status = runOther(child);
            switch(status) {
                case Success:
                    if(child.sibling.valid()) {
                        data.child = child.sibling;
                    } else {
                        clearData();
                        return Success;
                    }
                case Failure:
                    clearData();
                    return Failure;
                case Running:
                    return Running;
            }
        }
    }
}

The fun part about this is that the init function declares state the Behavior needs, which is stored on the entity’s component during execution. If the Behavior doesn’t need state, the code to handle that is entirely removed. It’s also fully typed, so for the data parameter in execute I get proper code completion and error checking.

Also if you notice the runOther and clearData, they aren’t declared in every Behavior, and they actually aren’t even declared in the base Behavior class. How so? The macro that processes the class will check what needs to be done and only include the code if it’s actually needed, with exact inlined code for the logic, right there where it’s called. That effectively means there’s zero runtime overhead for unused features and when the behavior runs, it’s actually just a single method without any function calls.

Practical Test

First actual use of all this was taking our Doggy for a walk. He keeps running around a log, when he notices the hero, starts circling around him, but will return back to running around the log if he gets too far from it.

I also quickly added support for “barks” for this test. They are a way for an NPC to convey emotions and such.

All that is powered by this behavior tree:

<Sequence>
	<MarkPosition mark={'spawn'} />
	<UntilFail>
		<Selector>
			<Sequence>//either find interest and circle it until too far away
				<FindInterest radius={70} interestingEntities={interestingEntities}/>
				<BarkAction engine={engine} type={BarkType.Icon(ExclamationRed)}/>//bark when found
				<Parallel>
					<Sequence>
						<UntilFail><DistanceSmaller value={200} from={'spawn'} /></UntilFail>
						<AlwaysFail><BarkAction engine={engine} type={BarkType.Icon(Question)} fast={true}/></AlwaysFail>
					</Sequence>
					<CircleAround />
				</Parallel>
			</Sequence>
			<Parallel>//or follow path, if close enough
				<DistanceSmaller value={150} from={'spawn'} />
				<AlwaysSucceed><FollowPath path={paths.get('tree_log_walk')} /></AlwaysSucceed>
			</Parallel>
			<Sequence>//otherwise we are lost, run home!
				<Parallel>
					<AlwaysSucceed><FollowPath path={paths.get('tree_log_walk')} /></AlwaysSucceed>
					<UntilSuccess><DistanceSmaller value={50} from={'spawn'} /></UntilSuccess>
				</Parallel>
				<BarkAction engine={engine} type={BarkType.Icon(HeartFull)}/>
			</Sequence>
		</Selector>
	</UntilFail>
</Sequence>

And this is how it looks compiled:

var forest = [];
forest.push(new ai_behaviors_Sequence(forest,0,-1,1));
forest.push(new ai_behaviors_Run(forest,1,2,-1,0,1));
forest.push(new ai_behaviors_Run(forest,2,3,-1,Math.PI,1));
forest.push(new ai_behaviors_FollowPath(forest,3,-1,-1,paths["tree_log_walk"]));
forest.push(new ai_behaviors_Sequence(forest,4,-1,5));
forest.push(new ai_behaviors_MarkPosition(forest,5,6,-1,"spawn"));
forest.push(new ai_behaviors_UntilFail(forest,6,-1,7));
forest.push(new ai_behaviors_Selector(forest,7,-1,8));
forest.push(new ai_behaviors_Sequence(forest,8,18,9));
forest.push(new ai_behaviors_FindInterest(forest,9,10,-1,interestingEntities,70));
forest.push(new ai_behaviors_BarkAction(forest,10,11,-1,engine,gui_BarkType.Icon(8)));
forest.push(new ai_behaviors_Parallel(forest,11,-1,12));
forest.push(new ai_behaviors_Sequence(forest,12,17,13));
forest.push(new ai_behaviors_UntilFail(forest,13,15,14));
forest.push(new ai_behaviors_DistanceSmaller(forest,14,-1,-1,"spawn",200));
forest.push(new ai_behaviors_AlwaysFail(forest,15,-1,16));
forest.push(new ai_behaviors_BarkAction(forest,16,-1,-1,engine,gui_BarkType.Icon(9),true));
forest.push(new ai_behaviors_CircleAround(forest,17,-1,-1));
forest.push(new ai_behaviors_Parallel(forest,18,22,19));
forest.push(new ai_behaviors_DistanceSmaller(forest,19,20,-1,"spawn",150));
forest.push(new ai_behaviors_AlwaysSucceed(forest,20,-1,21));
forest.push(new ai_behaviors_FollowPath(forest,21,-1,-1,paths["tree_log_walk"]));
forest.push(new ai_behaviors_Sequence(forest,22,-1,23));
forest.push(new ai_behaviors_Parallel(forest,23,28,24));
forest.push(new ai_behaviors_AlwaysSucceed(forest,24,26,25));
forest.push(new ai_behaviors_FollowPath(forest,25,-1,-1,paths["tree_log_walk"]));
forest.push(new ai_behaviors_UntilSuccess(forest,26,-1,27));
forest.push(new ai_behaviors_DistanceSmaller(forest,27,-1,-1,"spawn",50));
forest.push(new ai_behaviors_BarkAction(forest,28,-1,-1,engine,gui_BarkType.Icon(5)));

Bonus video – testing performance – so far so good.

1 Like

You can see all the devlogs and sign up for the newsletter to get the next ones directly to your email at https://antriel.com/rpg/. :slight_smile:

Cool! Thank you. :+1:

Devlog 4

Since the last devlog there were 17 commits between 2019/02/26 and 2019/03/06, totaling 51 commits. I have worked 39 hours during this period, bringing the total to 122 hours.

Shaders

This time I took a bit of a rest and did something I was itching to try for weeks: shaders.

They are programs that handle the actual process of putting pixels on our screens. Most 2D engines abstract them away and there’s mostly no reason to touch the actual shaders. But you can achieve really nice effects with them, and Phaser does expose a simple way to use custom shaders.

I started with a simple grayscale test, but that’s not very exciting. My initial goal for shaders is to do color grading post-process. That will allow me to change the feeling of the game in various areas by changing the colors. That’s accomplish very simply, by using a color lookup table.

The shader looks at what color a pixel has, looks into the lookup table using the color as address, and applies the value there instead.

The lookup table is made by an artist by modifying a screenshot of the game in some tool, and applying the same modifications to default lookup table (one that maps to the same color).

I’m no artist, so I just used something random I found on the internet for testing:

shader_lut

AI Debugging

When doing first practical test of my AI system it made me realize that tracking down issues is very difficult. I had no easy way to see how the behavior tree is actually traversed at the moment. If my AI wasn’t doing something it was supposed to, I couldn’t be sure if it’s issue in some condition, the actual behavior for that activity, or something entirely different.

So apart from doing various improvements and fixes I thought about how I could debug the behavior trees at runtime. I ended up creating a very primitive system (for now), where the game sends tree data and its current status to a server that’s listening over WebSocket.

I investigated a couple of options and ultimately ended up using zui made by Lubos Lenco which is built on top of Kha framework. I used it because it was very quick to set up and because I wanted to look into Kha ecosystemm as I might have a use for it some day.

Zui is immediate mode user interface library. What that means is that I can very easily map the tree data and status into what’s displayed on screen.

Connected to the practical test from the previous devlog it looks like this:

ai_tool

Having this new tool I didn’t hesitate and made a slightly more complex AI. We can now play fetch!

Next time I will talk about pathfinding, as without it our game of fetch might get a little awkward.

ai_todo

See all the devlogs and subscribe to the newsletter.