Jump to content

[Phaser 3 – WIP] Small Untitled RPG


Antriel
 Share

Recommended Posts

This is a copy of the first devlog I published on my blog a few months ago.
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/.

Devlog 1

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.gif

I’m using  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.

Subscribe to newsletter.

Link to comment
Share on other sites

  • 9 months later...

Small Untitled RPG Devlog 2
Since the last devlog there were 10 commits between 2019/01/27 and 2019/02/15, totaling 21 commits. I have worked 28 hours during this period, bringing the total to 48 hours.

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

message_editor.png

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

Video Link

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.png

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.gif

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.

See all the devlogs and subscribe to the newsletter.
 

Link to comment
Share on other sites

Small Untitled RPG Devlog 3

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.

See all the devlogs and subscribe to the newsletter.

Link to comment
Share on other sites

Small Untitled RPG 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|400x300

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|800x350

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|400x300

Link to comment
Share on other sites

Small Untitled RPG Devlog 5

Since the last devlog there were 5 commits between 2019/03/08 and 2019/03/13, totaling 56 commits. I have worked 20 hours during this period, bringing the total to 142 hours. Yes, this devlog is released over a year late.

Playing fetch is fun, but lack of pathfinding turns minor obstacles into insurmountable ones. So I went down the rabbit hole studying how to implement one.

Tiled based games (which this is) often use a simple A* algorithm to search through the tiles. That is pretty easy to implement and works well, but if you remember the first GIF I showed back in the first devlog, you might notice the character is able to walk around the sign post much closer than just a whole tile size away. That’s because I’m not using a simple walkable/non-walkable tiles collision system.

collisions|480x360

On top of having more complex collision shapes, I also wanted entities to move naturally towards the target, not from tile to tile. This is achieved by using a navigation mesh. With this approach, I can still use A* to look for paths between two points, but the path will be based on arbitrary convex polygons, not fixed-sized tiles. This can also be faster than tile-based approach, as there’s less nodes to search through.

First problem to solve was generating the navigation mesh. I didn’t want to create one manually, as that would be error-prone and a lot of tedious work. I assumed this would be simple and went into it pretty naively – by cutting out collision rectangles.

navmesh_cut|400x300

Turns out it’s not that simple. This would generate a lot of thin slices. Determining if an entity fits through would also be non-trivial. I thought about optimizing it, but trying to avoid reinventing the wheel, I turned to google for some proper research. Turns out this is quite a big topic, but I found an approach I liked a lot, called Funnel Algorithm. I’m not sure who came up with it first, but a very good resource I found is a thesis called Efficient Triangulation-Based Pathfinding by Douglas Jon Demyen.

I didn’t read it all, as a lot of the math concepts would take me too long to properly understand. But the algorithm itself seemed doable. Everything needed was described in the thesis. While it all seemed simple in principle, it wasn’t a trivial topic once I started thinking about implementation. From generating a triangle mesh, finding a path through the triangles, optimizing it for straight lines and avoiding cutting corners, it added up to quite a task. I estimated it would take me at least a month to implement (looking back, I think it would take longer).

Luckily, there were some implementations of the necessary parts in the open source world. I tried looking for some good-enough looking resources that wouldn’t be horrible to port to Haxe, and to my surprise I found a library implementing it all in a nice package. Enter hxDaedalus.

navmesh|640x480

It’s a mess mesh! It all turned out great and I got it working in a day. Now we can play proper fetch:

Subscribe

See all the devlogs and subscribe to the newsletter.

 

Link to comment
Share on other sites

  • 3 weeks later...

Great job man! It's looking very nice.

I am creating an RTS Template with Phase

Great job man! It's looking very nice.
I am creating an RTS Template with Phaser. I spent One Month trying to implement Steering Behaviors to My Units, but unfortunately, I was unable to implement it with the full functionality. I did implement the Flocking Behavior but it was So awkward that I stopped for a while. I have learned a lot in the process tho, I found lots of very useful articles that helped me in the process. One of the create articles that I read, lead me to a video from GDC Vault talking about the Starcraft 2 Group Movement, in that video, I heard for the first time about this "Efficient Triangulation-Based Pathfinding" that you talk about. It seems pretty cool.

I'm really happy that I found this article, maybe now, with a little bit more experience than previously, and with the help of this JS implementation of the Triangle Path Finding, I will be able to finish the work I started.

Maybe in a couple of weeks, I will post the Project here in the Forum.

Thanks for your time to write it down.

Edited by Sky Alpha
Link to comment
Share on other sites

Small Untitled RPG Devlog 6

Since the last devlog there were 33 commits between 2019/03/19 and 2019/04/09, totaling 89 commits. I have worked 68 hours during this period, bringing the total to 210 hours.

As I started adding more things to my test map, I decided to first improve the workflow and prepare for future development. Tiled supports external tilesets – standalone data files that can be re-used between different tilemaps. Phaser currently doesn’t support them though, and in general there’s no reason to include all of tilemap data in the game. We can do better!

So, first things first, I switched my test map to use external tilesets. That meant I couldn’t load the map directly in Phaser. So I wrote a small tool that loads the Tiled JSON maps and the external tilesets it now references. Then it takes out all the useful data (tiles, layers, collision boxes, object types and positions, etc.) and saves it all into a custom JSON file. Then I had to somehow load it in Phaser. Which mostly meant copying over its Tiled loader code and adjusting it for my new format. I would be lying if I said it went without any issues:

tileset_bug|430x288

Now I had full control of everything and could do other necessary things. When rendering, due to floating-point errors, GPU can sometimes sample a pixel too far, causing an ugly effect that’s called pixel bleeding. To fix it, tiles on the texture need to be extruded, which means the pixels on the borders need to be repeated. So I implemented that.

I also took the time to pre-process the geometry used for physics/pathfinding. Boxes near each other could be combined into a single one, improving the generated navmeshes. I used another great Haxe library. A bit of merging and triangle decomposition (as mentioned before, naviagation mesh needs convex polygons to work), and this was the result:

optimized_navmesh|500x300

Done with the necessary things, I moved onto the not-so-necessary. For performance reasons it’s also better to use a single texture, as opposed to many smaller ones. Although with modern GPUs it’s not really true anymore, and it’s not like I was hitting any performance limits… but hey, I will take any excuse to code fun stuff. So apart from tile extruding I also added:

  • Merging: Let’s put everything into single texture (for now anyway). Tiles, sprites, animations, text, everything. “My name is Texture Atlas, for we are many sprites.”
  • Deduplication: Tilesets often have the same tile on multiple places, to make it easier for designers to find related tiles. We only need the one, so let’s automatically check for duplicates and remove them.
  • Filtering: We aren’t using every tile from the tilesets. Why drag them around?
  • Whitespace removal: Spritesheets use grid layout, each individual sprite has the same size. That makes finding the correct sprite, and therefore animation, easier (and historically, it saved memory – something we don’t need to care about anymore). But it means a lot of empty space being left behind. We can remove it, in exchange for storing more data about the texture atlas.

texture_atlas|800x600

There were some funny bugs along the way. This is so-called Phaser’s companion cube. It fills the empty space in your heart, and the good doggy knows exactly what you need:

spritesheet_load_bug|640x480

During this time I also wrote and published the second devlog and did some improvements to my blog (adding some Open Graph tags if I remember correctly).

Link to comment
Share on other sites

Small Untitled RPG Devlog 7

Since the last devlog there were 13 commits between 2019/04/09 and 2020/04/04, totaling 102 commits. I have worked 81 hours during this period, bringing the total to 291 hours.

Wow. Devlog for a year of work. Yes. So what happened? Mostly I was busy with other unrelated gamedev stuff. But it is cool stuff, that I will probably implement in this RPG too. I practically didn’t touch the RPG since June 2019 until March 2020.

Debug Draw

After improving the navmeshes last time, I added some debug drawing, so that I can see what’s going on. I’ve went with super Tiny pixelart font. In hindsight, I will have to change this up for something more readable. We don’t need the debug stuff to be pixel perfect.

debug_draw|480x240

Combat Loop

Then, excited, I started thinking about combat. Many decisions were made, many are still open for testing and future design. For now, the goal is to have turn-based combat that happens in the same environment as the rest of the game. No battle screens, nor side-by-side battles. This might prove to be a bit challenging in a 2d game, but should be doable.

I added templates to the source Tiled map, so that I could easily describe enemies on the source map. Then I’ve had to adjust my map format for the new entity combat data. Afterwards I did a bit of code design and implemented the logic for a basic combat loop. Without any real AI for now (or rather move-randomly-debug-AI), but I plan on using my behavior tree implementation for this too.

combat_loop|640x480

The Future

I was excited about finally adding combat, but I took some time to go over my plans and realized there’s a lot of things I need to do before combat is needed. I looked back at my notes and planned what should the first release look like. I’m not going to talk about it now, but it will be a kind of tech demo. In either case, it won’t be needing combat, but it will need a bunch of other little things.

It was also about time to start doing some basic marketing. So I’ve looked into that, did some updates on my blog, wrote more devlogs and started a newsletter (which you can sign up for here if you yet didn’t). I’ve used EmailOctopus for the mailing list support, because they have nice free limits. I also like that they use Amazon Simple Email Service with my own account, which will keep the cost much lower if when the list grows to hundreds of thousands of subscribers (it sits at a neat 13 at the moment).

For my blog I’m using static site generator Hugo, and I write the posts in markdown. I’ve leveraged that and created a very simple mjml theme to convert the markdown source into HTML emails that I send out in the newsletter.

That was the last thing I did last year. Then last month, finally, I’ve started dedicating some time to my soon-to-be-titled RPG. First step was catching up with devlogs, which is now done. As I’m writing this, there’s nothing further developed yet. I did spend some time on converting the devlogs markdown source into format I can paste into various forums (like bbcode). Turns out parsing markdown is far-from trivial, but using haxe-markdown got me where I wanted to be (although not without issues).

Next I should continue down the marketing path. I need to build a website, spam enlighten more portals with my devlogs, make a discord server, figure out some logo. Then there’s a bit of code maintenance to be done. Well, alright, a lot of code maintenance. But after that, it will be back to the fun things.

Link to comment
Share on other sites

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.
Note: Your post will require moderator approval before it will be visible.

Guest
Reply to this topic...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

Loading...
 Share

  • Recently Browsing   0 members

    • No registered users viewing this page.
×
×
  • Create New...