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.
All of these spells work. All systems shown within the screenshot work. __DEFAULTEMPTYROW__ is an edge case handler.
That’s the second half of the functioning spells.
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.
Next: More on AMO