1.) The UI. The UI is definitely not the prettiest thing, but as this project was entirely a prototype to get ready for the “real” project, I was willing to just make something functional, albeit not pretty.
2.) The inventory system is only 10 slots, but every item can have multiple features:
ItemID – The ID that the item attempts to emplace.
Unique – If the item is unique, more than one stack cannot exist.
Stackable – If the item is stackable, more than one item can be placed into one stack.
StacksTo – If the item is stackable, this is the cap for its stacking capabilities.
It also handles all combination cases, such as…
A non-unique stackable item going over the StacksTo cap will overflow into the next stack.
A unique non-stackable item will check the entire inventory before emplacing to ensure the ItemID doesn’t come up later.
A unique stackable item will check the entire inventory before emplacing to check for the ItemID and will handle overflow.
3.) Respawning. Respawning works for players, but death does nothing to NPCs. They are unstoppable!
To start with, AMO is short for “Arena Masters Online”. It was originally intended to be a multiplayer PVP arena game with traditional MMORPG style combat. While this project was a Blueprint/Code hybrid project, this project is probably 99% Blueprint. I will indicate the systems that are not. I will try to screenshot as many systems as I can, but if you want to check out the project in all of its crash-prone prototype glory, click here. There have only been a couple changes in the past 6 months, and those were all updates to the spell table and updating the project and solution to 4.12.5 compatibility. If you download the project, keep in mind that the project does not contain all of the assets that appear in these clips. Some of the assets are purchased from the shop.
First, a really quick introduction: A three way fight, followed by an explanation of the AI.
It isn’t the most visually stunning fight, but allow me to explain what is happening. I am playing in the editor itself. The two enemies are enemies of both myself and each other. Being that everyone starts at 5% health, their first reaction is to find a hiding spot to heal up in. I target one of the AI and follow him. As you can see, he runs to a spot that is out of line of sight of everyone and begins casting Tranquility, which creates an enormous collision sphere. Tranquility then applies the Tranquility HOT to the 5 lowest health targets every tick. In this case, he is the only one, but what if there are, say…. 84 AI running around in this arena, but they’re only divided into two teams?
As you can see, the frame rate is horrible, and they also get stuck on each other a lot, but notice how I’m not casting, but I’m getting healed, as are quite a few others. This is because one AI started casting Tranquility, and it applied to the five lowest health units within the AOE. You can also see by the top left that the AI is announcing what spell it is casting. How many spells in total are there?
The AI will only cast two: PC_DruTranquility and TEST_Frostbolt. The AI’s behavior is very erratic, I will admit that. That is because the AI wasn’t setup to actually fight in the prototype. They had very, very primitive logic.
That is likely illegible, so I will explain. The top of the tree is the root followed by a selector named “RealRoot” and then a selector. You can ignore the “RealRoot” selector. The unnamed selector is the important one. It will attempt to go into the first sequence node. If “IsEnemyVisible” is set, it will check its own status. If it is defensive (< 35% health), it will attempt to run away and find an enemy hiding spot by using an EQS query. The EQS is very basic.
Yes, it is called TestEQS. I apologize for the name, but like I said, this is a prototyping project, and this was my first venture into EQS. But this EQS does as follows:
Create a pathing grid of 1500 units with 100 space between with a 2048 height projection.
Discard all unreachable nodes. We don’t need the system to be doing calculations to locations that it cannot reach.
It then attempts to do a basic visibility line trace to enemies from individual nodes. It does not score on this, it simply discards nodes that are out of line of sight of an enemy.
At this stage, the only pathing nodes we have left to choose from are ones out of line of sight of the enemy.
We now measure the distance to this node from the querier, but we prefer lesser. So, we want the closest node to the AI. We score this on an x1 scale.
We now measure the distance from this node to the enemy unit, but we prefer greater. We want the furthest node from our enemy. We score this on an x1 scale.
We then take into account the facing direction our enemy. We want to prefer spots behind the enemy or on the peripherals if they are close enough. We score it based on this factor.
This means the AI’s hiding spot will be an out of line-of-sight node that is somewhere in the middle of being close to the AI and far from his enemy. This generally generates spots that are very close to walls or other surfaces that obstruct LOS.
Hence, the very vertical, oddly shaped map I was testing it in. As you can see in the first video, the AI stopped running down the steps to heal because he had already obstructed LOS there. That spot met all of the criteria.
If they went on the offensive, the AI was much simpler (perhaps to a fault)
It would poll all actors of type ArenaCharacter within 2000 units of the AI. It would then check for LOS existing. If LOS does not exist between the enemy and the AI, the enemy is discarded. Then, once it has all enemies in LOS, it prefers enemies that are further away.
This was designed specifically for ranged caster AI that only used 2 of the original 4 spells (the other two being TEST_FlameJet and TEST_Firefield). TEST_Bloom came a lot later, but I will explain that then.
The reason the AI was built to prefer far away targets is because the caster is more likely to be able to escape a far away target than an up close target. If it could find no targets in line of sight…
Above is the “search for enemy” EQS. That’s all it does. It finds the furthest location on the pathing grid (size 1000, 1024 height) out of line of sight and goes towards it. If everything is in line of sight as far as it can tell, it will prefer the furthest spot that is in line of sight.
The logic for this basic search was that if everything is in line of sight, the furthest point is the most likely point to take you to an area with more cover and hiding spots. This is a pretty big assumption, but keep in mind, most of the areas that currently existed within the game were small, tight corridors where this assumption worked.
It would regularly get the AI caught in a loop of spots and never get out.
Those are the rest of the traits for each ability. Almost every trait on the other two slides is functional.
Let’s touch on some of the more important ones.
RawSpellID – This was a storage mechanism so that I could replicate the lookup ID in the table without needing to replicate the entire spell. I could send “PC_DruReplenish” to all clients, and then all clients knew to look up PC_DruReplenish for all effects.
Spell Logic – This was the actor that was spawned by the spell.
Casting Type – Cast, Channeled, Charged. Cast spells went off upon reaching the target cast time. Channeled spells went off multiple times throughout the cast. Charged spells could be released early with reduced effectiveness.
Disable Type – None, Silence, Disarm, Either, Both. None means the spell can always be cast. Silence means that the spell can be cast while disarmed but not while silenced. Disarm means the spell can be cast while silenced but not while disarmed. Either means that the spell cannot be cast while either disarmed or silenced. Both means that the spell cannot be cast while both disarmed AND silenced.
Aiming Type – Frame Targeted, Skill Shot, Ground Targeted, Auto Targeted, Toggled. Frame targeted requires a target of some kind. It has some smart targeting features. If you are casting a friendly-only spell while targeting an enemy, it will attempt to target the enemy’s target. If the enemy’s target is also an enemy, it will cast on you. It does this for many different circumstances. Skill Shots shoot directly ahead. Ground targeted are exactly what they sound like – Ground targeted. Auto targeted spells are generally self centered AOEs, such as tranquility. Toggled was extremely buggy and couldn’t be trusted to work in most circumstances.
Player Targeting Rules – Enemy, Friendly, Friendly (Other), Self, Either. Enemy could only be cast on enemies. Friendly could be targeted on yourself or any other friendly unit. Friendly (Other) could only be used on friendly targets besides yourself. Self could only target yourself. Either could target friend or foe. In the C++ edition, I added “Enemy or Self” to the list.
Damage Type – There were 18 damage types, although most of the types on the picture are either physical, shadow, healing, absorb, or nature. Most of the damage types, admittedly, do not have a difference in this version (outside of healing inverting the damage).
Magic Schools – This was only used for lockout purposes. Yes, interrupts existed and worked (PC_AssPommelStrike is an interrupt).
Base Cast While Moving – Can this spell be cast while moving?
CD Line Name – Most of the time, this one is “None”. But this one is sometimes set to something else. Let’s say “PC_BerRecklessness”. When you cast PC_BerRecklessness, it triggers a CD Line called “BerRageLine”. This means that ALL BerRageLine abilities are on CD for the duration. This is for situations where two or more abilities all share a CD.
CDs Also Triggered – Most of the time, this array is empty. When it isn’t, it puts all abilities listed on CD for a custom time. So, if we decided PC_BerRecklessness and PC_BerRampage shouldn’t be in the same line, but they shouldn’t be usable simultaneously, we may decide to handle it through this. We may put “PC_BerRampage” on CD for 8 seconds so that it can’t be used within 8 seconds of PC_BerRecklessness.
SelfEffects and TargetEffects do not work.
Casting Particles – I split the particles into 3 categories: Hands, body, feet. They attach to the bones at the respective positions as well.
Casting Particles Offsets – I added vector offsets to the particles for hands, body, and feet if you wish to adjust where the particle is.
Targeting Scale doesn’t work.
Damage scaling doesn’t work as levels don’t exist.
Resource cost scaling doesn’t work as levels don’t exist.
Base level doesn’t matter as you cannot acquire abilities.
Cast animation worked until I accidentally overwrote the animation sequence with a blank animation sequence during a hard drive transfer.
LookupID is the lookup ID for the spell’s damage range on the SQLite table.
IsBuilder – Some classes have abilities that spend mana and some have abilities that build mana.
IsVariableSpender – This is for abilities that do not spend a fixed amount.
SpendingIntervals – This is the amount of mana that each incremental level increases by.
MinimumSpend – This is the minimum amount that can be spent by the spell.
MaximumSpend – This is the maximum amount that can be spent by the spell.
ModifierIDs – These are effects that the game will check the caster or target for. To stick with the Berserker examples, perhaps an ability can only be cast while Recklessness is up. This is where that would be handled. Or perhaps while Recklessness is up, this specific ability costs 50% less. This is where it would be handled. The big thing is that these aren’t always necessary. On the other hand, it will return true if ONE necessary feature is found, but not if ALL necessary features are found. It is better to use AlternativeResourceID for this purpose.
Alternative Resource Spent ID – Let’s say that every time you cast PC_CleSplashHeal, it gives the cleric a stack of Divine Favor. Divine Favor stacks to 10. For some spells, Divine Favor may be required. This is where the ID of that required resource would be listed. This is separate from ModifierIDs because ModifierIDs was a very binary solution with, as mentioned in the end of the paragraph, a difficulty dealing with multiple requirements.
MinAltResource – This is the minimum amount that can be spent by the spell.
MaximumAltResource (off the end of the image) – This is the maximum amount that can be spent by the spell.
I’d like to take a few moments to talk about “ModifierIDs”, though.
That is what the modifier ID struct looks like. In it, you see 5 options:
BuffID – Indicates the ID of the buff it is looking for.
IsOptional – As mentioned earlier, if it is not optional, only one non-optional buff is required.
IsSelf – Checks self or checks target
IsPositiveBuffID – If it is a positive or negative buff ID
AbsenceRequired – If it is required to NOT be on the target.
To use an example from World of Warcraft, Paladin Blessing of Protection would have the BuffID of Forebearance (whatever that is), it would NOT be optional, it would not simply be self, it would be a positive buff ID, and absence would be required.
Here’s a short video going through some of the blueprints required to make this happen.
If you want to see them all, the source is available up at the very beginning. Hopefully you can find some use or inspiration from them. Yes, the drag-and-drop works in the inventory and spellbook. The inventory mostly works.
Here’s a short video showing how many blueprints there are total:
I chose to start working on the C++ version of the project whenever UE4.11 preview 4 was pushed into the main branch and I accidentally updated, corrupting the project for a long time (it would crash when I made any changes to the Blueprints). I would have loved to continue working on the project, but by the time this project was functional again, the new C++ project was well on its way.
Note: I will be making some generalizations in this article. This is not going to talk about every MMO ever made, and I will try my best to indicate such. I may fail at points, but I hope you do not hold that against me.
When I talk about “popularity”, I’m talking about the popularity from a developer’s perspective not the popularity from a player’s perspective.
The biggest issue that will need to be tackled by MMOs in the upcoming years isn’t the focus on endgame. It isn’t the themepark vs sandbox debate. It isn’t PVE vs PVP. It isn’t soloable vs group-only. It isn’t even instancing or open world.
The biggest issue that MMOs will need to solve is how to get past an inconsistent design philosophy.
MMOs since the dawn of the genre have been plagued with this problem. This problem has grown to the point where it is difficult to fend off, though.
When I say inconsistent design philosophy, I mean that the hypothetical game contains multiple subgames that do not seem to be based in the same philosophical groundings as other subgames.
For a consistent design philosophy, I would like to look at vanilla Aion. In the case of vanilla Aion, the game was considered a very time consuming, grind heavy MMORPG. With that said, it also taught you from the very beginning that this was to be expected.
While this did drive some people away (and one can easily argue the merits of a system that chases people away), the game was very clear in its messaging. Accomplishments would be very heavily weighted based on time put in. This meant that players who reached level 50 would not suddenly be greeted by a very jarring, alternate experience to what the game had presented to them up to that point.
On the other hand, there is the inconsistent design that is present in many other MMOs. While I will not name specific names in regards to which MMOs violate this policy, I am sure you will be able to think of some that match this description. These are games that start by giving you an experience that is very narrative-driven and soloable. Once you achieve max level, you are only given the option to participate in raids or another activity that either demands high levels of skill or vast quantities of time. This isn’t a bash on soloable or narrative-driven MMO experiences, but the opposite (raid/party centric leveling experiences that result in soloable/narrative-driven endgame) is much, much rarer. In fact, I’ve never seen it in action.
The problem within this inconsistent design is created by violating the core principles of the heuristic ladder(s).
In the MMO genre, though, there are two ways that PVE difficulty is generally measured:
Amount of attempts to kill
Time spent is an unusual metric in the context of a traditional heuristic ladder because the amount of time that a player spends playing the game doesn’t necessarily correlate to any other aspect of the game. With that said, the principles one should adhere to regarding heuristic ladders are generally applicable to time-sink mechanics as well.
A few other factors can be argued, but these two measurements compose what I consider the two heuristic ladders of MMO progression. One of the core aspects of a heuristic ladder is that it is much easier for a player to move down the ladder than up it.
In simpler terms, an individual who is able to find 16 hours per day to play the game can easily accomplish something that is aimed at an individual that plays 1 hours per day. A guild that is competing for world firsts is going to easily accomplish something that is aimed at a guild that is competing for world 2000th. The opposite is not true, though.
A Consistent Experience
When players are leveling, they are learning, whether consciously or subconsciously. They are learning not only the mechanics of their character, but they are also getting a feel for reward mechanisms, reward timings, game-specific gating mechanics, and how the game presents content to them in general.
An inconsistent experience is one that teaches the player all of these things, reinforces these things many, many times, and then suddenly changes them once the player reaches an arbitrary point in their character’s life cycle, which is quite frequently the max level of the game. This sudden change will disrupt any form of flow the player was experience. It will be a very jarring, noticeable shift relative to previous experiences within the game. This tends to create one of two negative responses from players within the game:
The Top Going Down
A skilled player or a player that spends (or wishes to spend) a lot of time in the game suddenly being asked to climb downwards on a heuristic ladder can experience varying degrees of boredom, but it is feasible for them to do so.
Popular mechanics that ask players to move downwards on the heuristic ladder include mechanics that are time-gated (such as dailies, weeklies, or NPC missions that require X hours to complete) and mechanics where there is minimal chance for failure for the majority of players.
A common use for these two mechanics is found in the form of the daily quest (which is a form of currency faucet).
The Bottom Going Up
A player that is not as skilled or does not have a lot of time suddenly being asked to perform a task that requires world class skills or 16 hours per day of dedication will likely experience varying degrees of frustration. Unlike the top going down, these players aren’t likely going to handle the transition well, if at all.
One of the most popular mechanics that ask players to move upwards on the heuristic ladder is raiding. Raiding is generally a time consuming activity that will kill the player many, many times before the player is able to advance.
The Need For A Unified Experience
All of these points boil down to the need for a unified experience. The leveling experience should exist to teach the player what to expect. It should be teaching the player the reward mechanisms. It should be teaching the player the ins-and-outs of the game. Properly utilized, the player should know what to expect in the endgame by the time they get there. The player should not be suddenly asked to throw away all of their acquired knowledge to that point because the core experience within the game changes at an arbitrary level. The game’s focus should be on teaching players and remaining true to those teachings.
This unified experience shouldn’t simply be a mission statement that the team pays lip service to during conventions or during press conferences.
This unified experience should be the driving factor behind every decision. Every decision, every system, and every act should be done to establish and empower this unified experience.
That is how I have the lower level (which I called “pleb” in this case) structured. The service sends a request to the commander for a new destination. A more optimal way to set this up would be to have it be a task instead of a service and have it add the lower level AI to an array on the commander’s end. Whenever the commander discovers a new location to search, the commander would do a second EQS search using the array of waiting “plebs” as context for the search. This would allow the closest pleb to be selected, and this would mean that the pleb wasn’t asking for a duty every 1 second or so.
This is the version of the commander tree that I discussed in earlier posts. This one is OK for the most part, although the decision making for the data nodes could be improved slightly. Perhaps pick a random data node that exists already, and check it for line of sight to any other data nodes would save the work load and allow for a much more robust scan.
To boost this further, you would need to track which nodes even have an eligible point somewhere in their pathing grid. If they do not, remove them from the possible selections.
These are all untested hypotheticals, but if you wish to use this in a live project, I hope this helps in some way.
#1: Over vast areas, this system begins to bog down. Each EQS query is extremely expensive given the ever-increasing number of data nodes that are being used as context points. If your EQS query favors close points, the system is able to generate many more points without struggling. If it favors far away points, the system will begin lagging due to the massive quantity of nodes that the EQS queries are checking.
The above picture is what happens whenever you prefer long range locations. You get a much wider breadth of targets hit, but you also don’t search much of the town.
If you are willing to trade off some accuracy, I found that 1000 range with 250 distance between the points on the EQS’ pathing grid generator allows for many more data nodes to exist before the system starts bogging down.
I have not found an optimal solution to this problem at this stage.
#2: The trace on the EQS runs for infinity. This can cause issues going between areas that are separated by large swaths of open land.
#3: It does not factor in distance from any AI agents. If you are willing to factor in the distance from AI agents, you may be able to make it slightly more coordinated.
Note: This system is a work in progress. In part 2, I will discuss things that could be changed for a better result.
Goal: Create a multi-tiered AI that performs navmesh search operations distributed across many agents.
Description: For this system, I am trying to create an AI that would attempt to search every nook and cranny it could. Once a location is checked, it should not be checked again. This should create a systematic sweep of the entire area.
The AI should begin by using EQS to discover a location that has not been searched yet, and then the AI should go to that location, search it, and repeat this process until nothing in range has not been searched. The undiscovered point should be close to a discovered location.
The first step was to figure out the behavior I’m looking for. In this case, I want…
A location on the pathing grid
A location that has not been seen yet
A location that is close to the AI
A pathable location. That is to say that, while the roof of a house may be part of the pathing grid, it is not pathable because an agent cannot make it there.
The areas to be searched are distributed among participating AI
This will be a 4 step EQS process.
#1: Set up a pathing grid generator in EQS. I chose 1000 range with 100 distance between points. There are some unfortunate situations where 1000 range with 100 distance between isn’t good enough. Be careful when making this too large as we are going to be using the data nodes as context points, and you will have many of these in the world.
For the context, you will need to consider something if you choose to do this as well. If you want this system to run in any situation with no prior setup, you will need to do a context check off of an existing actor. If you are looking to do this without a preemptive Data Node, you will need to use the higher level controller (or one of the lower level soldiers) as the context to do the EQS check off of.
In my prototype, I placed a Data Node (mentioned in #2) where the higher level controller spawns at, and I set up a context that returns all BP_DataNodes in the level.
#2: I need to somehow keep track of all of the points the agents have visited. For this, I created a Blueprint that I called BP_DataNode. This Blueprint was originally supposed to contain data (hence the name), but it ironically stores no information now and is simply used as an indicator of the spots the AI has scouted.
For this step, I did a trace test. I traced from the pathing grid node to the context (same context as last step), and I had bool match set to true, even though what I was checking was whether it was out of LoS or not. This test is a simple “Filter Only” test. Scoring does not matter, as I want to eliminate all of the visible nodes, not give +1 to the ones I can’t see.
#3: Now I did the distance check. I used the same context as the previous two steps, and I measured the distance from the node to the context with a “filter and score”, although the filter portion was only set to a minimum of 0.0. The scoring was set to an inverse linear system so that closer points were favored.
#4: Finally, I checked if a path existed. If the node is unreachable, it should be discarded. This is also a filter only result. This test is optional depending on how the terrain is built. In my case, there were rooftops that would appear on the pathing grid, but they were not reachable.
This is the EQS I used to scout for undiscovered locations. The massive red downward arrow is pointing towards the initial data node (so that I could track where they spawned). The querying mode isn’t that important for it, although I prefer 25% to create minor variance.
The next stage was to figure out how I was going to distribute the nodes to the lower level agents. For this, I had the higher level AI perform the EQS, generate a node at the returned grid point, spawn a BP_DataNode, and add it to an array of nodes that have not yet been searched by an agent. This particular decision was made so that they all have access to the same list, and the list is synchronized.
The low level AI is very simple. It moves to its current target node. When it arrives, it asks the higher level controller for the next node. The low level AI has no decision making regarding which node it goes to. All of that is handled through function requests sent by the lower level AI to the higher level AI.
The red arrows may seem clumped up, but the grey boxes are 4-room buildings to test functionality. All of the red arrows point to data noes that are out of line of sight of all other data nodes, and the lower level agents are being sent to systematically check each of those points.
If there are any questions regarding this system, please don’t hesitate to ask in the comments. While the comments do require approval, this is to prevent spam.
In the second post on this subject, I will detail the problems this setup has realizing the goal AI mentioned at the beginning of this post.