Now that we're wrapping up the spell casting (our largest feature to date), I'd like to take some time to review the architecture and touch on some of the more challenging aspects of this change.
New Features
This covers most of the big changes we made, but there are probably a few aspects that are missing.
General Changes
- Reusable building blocks to define a new spells, spell effects, etc.
- Spell book window, spell bar, and spell cast window on player clients.
- Ability to scribe a spell scroll into your spell window.
- Ability to memorize a scribed spell onto your spell bar.
- Ability to cast a spell that is memorized on your spell bar.
- Animations for spell casting, including particle effects.
- Aggro logic for beneficial vs detrimental spells.
- Detrimental spells can apply different types of curses, which can then be cured from a beneficial spell.
- Restrictions can be defined so that certain spells cannot coexists on a character at the same time.
- Spells can fizzle based on the player's skill level
- Spells can be interrupted if the player moves too far from their original location
- Spells can be fully or partially resisted depending on the target character's resist stats.
- Refresh timers that prevent spells from being used immediately after they're memorized or after they have been used.
- Line of site & distance restrictions on spells.
Types of Spells
This seems like a good section to enumerate, as the above list simply contains the plumbing and support to make the system work. This covers the different types of spells that we support so far:
- Direct Heals
- Heal Over Time
- Direct Damage
- Damage Over Time
- Increase / Decrease Attack Speed
- Increase / Decrease Movement Speed
- AOE spells (detrimental & beneficial)
- Stat (HP, Resists, Strength, Stamina, etc.)
- Root
- Mesmerize
- Feign Death
It is worth noting that most of the above spell types are built using shared definitions / blocks.
For example, we could create an AOE haste + hp buff spell by setting the target type to one that includes multiple targets and then configuring the spell to use two spell effects: one to increase the attack speed, and another to increase the character's total hitpoints. We could then create another spell which only buffs the total hitpoints for a single target, reusing the same spell effect that was used for the aforementioned AOE spell.
And with that, lets get into the actual design and architecture.
Design & Architecture
To give an idea of how large of an effort this was, it took roughly 3 months to get all of the changes in when we can usually get a new feature out in 2-3 weeks.
Another interesting metric is our number of APIs, event types, and database tables. Before this change, we had:
- 70 APIs
- 29 Client Event Types
- 35 Database Tables
After adding spells, we're now at:
- 76 APIs
- 52 Client Event Types
- 48 Database Tables
Notice that we had a large increase in the number of client events and database tables, but a relatively small increase in the number of APIs. This is because there are very few actions a player can actually trigger (scribe a spell, memorize a spell, cast a spell, etc). Really, this was more an effort of keeping data between the client / server synchronized as well as handling asynchronous operations.
Data Modeling
This was one of the quicker aspects for us to design and implement. After a long session of planning out the tables based on our requirements, we ended up with a structure that was mostly similar to our current state (pictured below).

Hopefully we did a good job naming each field so that they're self explanatory. We probably have a few opportunities to consolidate some of our spell effect fields, but for now we'll see how things grow and whether such a change becomes worthwhile.
There are two interesting designs that I'd like to call out here: the different ParticleAssetIds on the spell_definitions table, and the concept of a "spell line".
Spell Particles
The 4 different "Particle Asset IDs" are used to control what particle the player client will display.
- Casting Particle - The particle that will be created while the spell is being cast
- Projecting Particle - If this is defined, this particle will be created at the caster and will project towards the targeted character upon a successful cast. This is used for things like the fireball spell, which shoots out from the player and explodes upon impact with the target.
- Primary Target Particle - If defined, this particle will be created on the caster's primary target when the spell cast is successful.
- Secondary Target Particle - If defined, this particle will be created on any target that was affected by the spell but was not the primary target.
The more interesting particle is the Secondary Target Particle. This allows for us to animate a different particle on secondary targets that is different from the particle on the main target.

Spell Lines & Restrictions
I felt that this design was interesting because it was something that we did not really think about until we were two months into the feature. This came about when we realized that we were lacking a solution for the following:
- Weaker spells should be over written by more powerful spells
- Certain groups of spells can be over written by spells in another group
- Detrimental spells can overwrite beneficial buffs
To support these requirements, we introduced the concept of having spell lines and restrictions between one or more spell lines.
Notice from our data model that each spell definition that is mapped to spell line includes a "rank". This rank, in addition to the caster's level, are used to determine whether an incoming spell is "weaker" than the current set of spells on the character.
For example, lets say that we want to have a single HP/AC buff that cannot coexist with any other HP buff. We would then create a restrictions between every HP buff "spell line" and the single HP/AC buff "spell line".
Insert(new SpellLineRestrictionEntity() { SpellLine = _spellLineDao.GetByName("Cleric HP Buff"), RestrictedSpellLine = _spellLineDao.GetByName("Cleric HP/AC Buff") }); Insert(new SpellLineRestrictionEntity() { SpellLine = _spellLineDao.GetByName("Cleric AC Buff"), RestrictedSpellLine = _spellLineDao.GetByName("Cleric HP/AC Buff") });
With our current implementation the restricted spell would overwrite the current spell. For example, if a spell from the HP/AC line were cast onto a player having an HP and/or AC buff, the HP and/or AC buff would be removed and the single HP/AC buff would be applied. We may update this so that the restricting spell doesn't always overwrite, but that will probably come as we start to focus more on content creation.
New APIs
- Scribe Spell
- Memorize Spell
- Cast Spell
- Remove Buff
- Remove Memorized Spell
- Get Spell Definition (will eventually replace with an artifact that we build and include as an asset on the client)
Most of these APIs were straight forward to start, but became more tedious as we wrapped up all of the finer details. The more challenging implementations came from those that were asynchronous: Scribe Spell, Memorize Spell, and Cast Spell. These were the APIs that would "initiate" an action, as in the decision and logic occurred at a later time on the server. For example, the player starts to cast a spell but the spell won't actually complete until seconds later.
Our approach for these APIs was to build an asynchronous type of handler for all spell actions. This worked well because a lot of the same properties exist between scribing, memorizing, and casting a spell. It also gave us a more generic way of restricting the player to a single spell action and checking whether that said action is complete.
public void HandlePlayerSpellAction(PlayerAggregateRootModel player) { var spellAction = player.GetSpellAction(); if (spellAction.ActionStatus != SpellActionStatus.InProgress) { HandleCompletedSpellActionEvent(player, spellAction); player.ClearSpellAction(); return; } if (spellAction.CompleteAt < DateTime.Now) { spellAction.CompleteAction(); // Send an event to the player HandleCompletedSpellActionEvent(player, spellAction); // Handle any side effects from the action if (spellAction.ActionStatus == SpellActionStatus.Succeeded) { if (spellAction.ActionType == SpellActionType.Scribe) { HandleSuccessfulScribeAction(player, spellAction); } else if (spellAction.ActionType == SpellActionType.Memorize) { HandleSuccessfulMemorizeAction(player, spellAction); } else if (spellAction.ActionType == SpellActionType.Cast) { HandleSuccessfulSpellCast(spellAction); } } player.ClearSpellAction(); } }
It is worth noting that this server side code does not run inside of Unity and is instead a C#.NET console application. During the optimization phase of this project we will likely convert a lot of these frequent checks into something more asynchronous (like Coroutines in Unity).
Whats Left?
Although we have most of the framework for spell casting in place, there are still a few pieces of the plumbing that needs to be finished:
- Scale the duration of a spell based on the player's level
- Ability to remove a scribed spell from a player's spell book
- NPCs can inherently cast spells for their class / level
- Authoritative checks for line of sight
The first two pieces are going to be relatively quick. No more than a couple of hours each. The last piece, authoritative checks for line of sight, will be a lot more challenging. We mentioned above that the server side code does not run inside of Unity. To handle authoritative decisions based on the physical layout of the world, we are running a single, headless "Authoritative" client per zone server that handles NPC movement, some of the NPC AI such as proximity aggro, and will probably be updated to handle line of sight checks.
More details about our client & server architecture will come in a follow up post, but this is a detail we leave for later on in the project as we may put the time in to either export the terrain into our server side code or move our zone servers into Unity entirely.
We also have a few different types of spells to add before we can call this task 100% complete. Here are a few of the more important ones:
- Resurrection
- Stun
- Memory Blur
- Teleport
- Charm (will need to wait until pets are added to the game)
With all of the above changes made (except for maybe the authoritative checks and the charm spell effect) we should be set to call this feature ready for the game's initial release.
Leave a comment