read manual online at
for support please contact
- How To
permalink: /manual title: “Overview” sidebar: title: “Manual” nav: manual —
This is the manual for Action Adventure Kit by SoftLeitner. Action Adventure Kit is quite the mouthful so I will be referring to it using the shorthand AAK.
Action Adventures are a very wide, loose genre with all kinds of different games and sub genres. There are some things that many of them have in common though. Lots of them have characters that perform actions. These might have items that change hands and attributes that define their capabilities.
AAK was made to provide a solid foundation for these common ideas within the genre so you can focus on what makes your game special.
Since AAK goes for a broad base of functionality rather than something more specialized it generally tries to do anything in the most default, unity-built-in way possible. For example the movement in the souls demo uses the default unity character controller and the intro is done using timeline. To accommodate for the fact that more special systems may be required for movement, inventory, actions, … AAK was made from the ground up with expandability in mind.
AAK is separated into multiple projects.
The Core Framework of AAK, always import this one.
Contains the getting started project. Recommended to learn about the various systems of AAK in a minimal environment before jumping into the more complex souls demo.
Contains the soulslike demo, import if you want to start by adapting this demo.
Start up Scenes/Title/SoulsTitle to start the game from the title screen just like the demo does.
To jump directly into the game open Scenes/Dungeon/SoulsDungeon for the level itself, add Scenes/Dungeon/SoulsDungeonTemp for the temporary parts like enemies and crates.
Scenes/Debugging/SoulsDebuggingGeneral is a useful scene for testing out all the actions and Scenes/Debugging/Enemies/SoulsDebuggingEnemies can be used to debug combat.
Scenes/Debugging/Interaction/SoulsDebuggingInteraction can be useful to synchronize the character with some object they are interacting with, this is done using timelines.
The models in this demo were made using blender and then exported to fbx for unity. You can find the original blend files and the used export settings in AdventureSouls/AdventureSoulsBlender.zip.
- Input System allows binding character actions directly to inputs
- Timeline used to provide a character action that waits for a timeline to finish
- Cinemachine used in a helper that performs camera locking using two virtual cameras
- Input System for all the player inputs
- Timeline for the intro and bonfire actions
- Cinemachine for the main camera
- Universal RP
- ProBuilder used to build the environments
This manual is meant to explain the concepts and ideas of AAK rather then any specific detailed API. For more detailed explanations of every class in the core framework and most of the demo please consult the code itself. I try to give a detailed explanation for the purpose of the class in the xml-doc of the class itself and explain every field of the behaviors in the tooltip.
The manual pages for the core systems of AAK are always split into a Core part that explains the idea behind the system and a Souls part that covers how the system was used in that demo.
The Souls section of the manual contains some additional information about how the AdventureSouls demo is set up and how it may be extended.
All GitHub repositories related to my unity assets can be found in the Softleitner Extras list. I generally try to keep the main ones updated but some of the minor ones may be out of date.
There will be one more smaller update that focuses on polishing the current core functionality and the souls demo. While working on that I will be planning out the second demo which I hope to get started in the following update. Feel free to let me know if you have some features or game types that you’d like to see in the future.
The quickest channels to reach me are mail and discord. Please feel free to reach out with any problems and questions. Feedback regarding the general direction of AAK and particular future features are also always welcome. Though I might not immediately be able to incorporate your requests I very much take them into consideration when planning out future updates.
If you can spare the time please consider leaving a review in the asset store.
permalink: /manual/releases title: “Release Notes” sidebar: title: “Manual” nav: manual —
This update continues building out the souls demo while polishing up the core framework. It also adds a getting started tutorial project that should provide a smoother experience at the beginning. There is also a new integration available on GitHub for the Kinematic Character Controller asset!
- Step by step getting started tutorial
- Pebble usable item in souls which can be thrown
- Two handing weapons in souls
- stances for two handed melee and bows
- assignable actions for all 4 shoulder buttons
- Bow and Arrow in souls
- damage is summed between bow and arrow
- easily customizable arrows
- Camera damping in souls demo
- SoulsBossArea not properly resetting on player death
- MotionAction without cost now works on characters without ResourcePool
- Namespace of SoulsHideHUDInstruction which unfortunately breaks references so it has to be reassigned or fixed in text
- MovementBasePersisted does not error if no Persister is set
- LockableCameraFreeLook can move a target group member instead of adding the lock point
- LockOnManager can directly parent a visual to the locked point
Starting from this version I will no longer upload a dedicated version for the 2021.2 tech stream. I recommend using the latest 2021.3 LTS version.
Various smaller changes made for the AdventureSouls Scene Connector integration example. The example is called ConnectorSouls and can be found on GitHub!
- TimelineAction and SoulsBonfireAction auto bind CinemachineBrain to PlayableDirector
- AnimationToggler can be moved instantly using SetA and SetB
- SoulsLadderAction EndingTop and EndingBottom events
- SoulsLoading can be configured to load additional scenes
- SoulsPlayerCharacter multi scene support
- saves current scene as checkpoint
- goes to loading screen on death in a different scene
- recovery is instantiated under SoulsCommons
- EffectPool and ResourcePool persist immediately when reset
- Shrine - New Souls Demo Stage!
- Trader and Talker NPCs
can be interacted with or attacked
- Sprout Enemy
causes poison damage
one way elevator with levers
moves character between scenes
- Trader and Talker NPCs
- Effects - New Core System!
set of behaviors that can be added or removed from characters
periodically causes damage, healed by the new moss item
doubles strength for a while, added by the new booster item
allows using items directly from the UI without equipping them
- Loading Screen
- intro can now be skipped
- NPCs can return home or patrol when idle
- additional events for characters on GenericTriggerArea/Item
- better gamepad support
- movement and resources are not saved unless they actually change
- picked up items are now displayed in a message box
- persistence can now be exported and imported from PersistenceContainer
- boss arena properly resets player position
- camera now uses cinemachine free look
An integration for the ink narrative scripting language is available on GitHub!
Depending on your depth of use this update will contain a varying amount of breaking changes! Using source control and removing any previous versions completely when upgrading is recommended.
- manual page for SoulsPlayer(includes step by step guide for replacing the model)
- Animator is now part of CharacterBase, AnimatedCharacterBase is therefore obsolete
- simplified model replacement for SoulsPlayer
- colliders moved to separate GameObject ‘Body’
- TimelineAction and SoulsBonfireAction can pass character animator to the PlayableDirector which lets us remove the dependency between environment and player model
- translation and rotation of model can be suppressed independently
- SoulsAttackAction no longer locks rotation until damage is activated
- MovementSaver can be overridden from outside
- BossArena uses this to properly reset the player if the game is quit during a fight
- intro sequence can now be skipped
- 2021 LTS compatibility
- usable items not being hidden if action is interrupted
- potential initialization order errors
- AttributePool value/stat dictionaries
- CharacterActionArea Bindings
- armor no longer vanishes at certain camera angles
permalink: /howto/gettingstarted title: “Getting Started” sidebar: title: “Manual” nav: manual —
Be sure to import at least the AdventureCore and AdventureManual projects to check out the getting started project. The scenes are located directly in Assets/SoftLeitner/AdventureManual/GettingStarted and the different asset used are all found in the subfolders.
The GSFinished scene contains the fully finished project, GSStepByStep has a deactivated gameobject for the state after each step and the GSEmpty scene only contains the environment and can be used to start from scratch when following this page.
First off we’ll just get a character moving around the environment.
- Create an Empty gameobject called ‘Character’ and reset its transform
- Add a GenericCharacter character component
Most games, including the souls demo, will probably require some custom logic in the character but in this example we can get away with using the generic one just to bind all the other parts together.
- Drag the GSRobo prefab into the Character and assign the Character field in the AnimatorProxy and the animator field in the character
The prefab just contains a little robot model with a couple animations set up. The AnimatorProxy will forward things like animation events and root motion to our character which allows us to have them on separate objects.
- Add a CharacterController and a CharacterControllerMovement movement component to the Character
- adjust the character controllers center and skin width
- assign the movement to the Movement field in the GenericCharacter so other systems can access it
- assign the GSRobo you created as the Pivot in the movement so it is rotated when moving
- assign the MainCamera as the Camera of the movement so it can translate inputs relative to the camera
- Add a PlayerInput component to the Character(Input System)
- for Actions select GSInput which can be found in the Other subfolder
- set its Behavior to ‘Invoke Unity Events’
- under Events->Default->Move create a callback that calls CharacterControllerMovement.OnMove
You should now be able to push play and move the character around the scene.
In this chapter we’ll set up a trigger mechanism that slows our character when it moves through a certain area.
- Add a GenericTriggerItem and a CapsuleCollider component to the Character
- Assign the Character field on the trigger so it is accessible to areas it enters
- Adjust the colliders size and tick the IsTrigger field
- Create a new Cube 3D Object and add a GenericTriggerArea
- Adjust the cubes size and material and tick its colliders IsTrigger
- Add the MovementSpeedMultiplier instruction to the area and set the Value to 0.5
Start the scene and move the character through the area to see that it is slowed down by the factor defined in the Instruction. The area adds its instructions to the character of any trigger item that enters it and removes them when it exits. The generic trigger items and areas are good for general purpose, when creating more specialized logic consider inheriting from TriggerArea and TriggerItem like, for example, the damage system does.
Here we will add the same kind of slow effect as in the last chapter with a status effect instead of directly applying it. This allows us to visualize the effect on the character and it would also be easier for other systems to check if the character is currently in one of these areas.
- Create an empty child in the Character and name it ‘Status’, this is where we will organize all the behaviors that make up a characters status
- Add an EffectPool component which will manage the characters active effects
- Assign the Character to the EffectPool to each other
- Set the Effects field on the Pool(used later when dealing with persistence)
- Copy the slow area from the previous chapter and remove the instruction
- Create handlers in the areas CharacterAdded and CharacterRemoved events
- Drag the GSSlow effect into them and select the EffectType.AddExternal and EffectType.Remove methods
- Create a new UI Text and add a EffectPoolText
- Delete the EventSystem Unity creates when you first add a UI component
- Adjust the Text size and position
- Assign the fields in the effect pool text
The effect assets for this chapter have been created upfront to streamline the process, the effect set and effect type can be created from the context menu under Create/Adventure. The most important bit is the effect Prefab which contains the effect itself with the slow instruction as well as the visual.
When you start the game now and move into the new effect area you should be slowed and additionally see a small blue sphere above the character. You should also see the effects name in the UI text you created.
Next up we’ll create some item pickups and allow the character to add them to its inventory. First up we’ll allow the character to start actions that it runs into.
- Create an empty child in the Character and name it ‘Actor’, this is where we will put all the logic that lets the character perform actions
- Add a MinimalCharacterActor which will manage the characters current action
- assign it to the character and the character to it
- Add a CharacterActionArea which lets the character start actions it collides with
- assign the actor to it so it knows where to start its actions
- Add a Trigger CapsuleCollider and a Kinematic Rigidbody for the action area
- Add a MinimalCharacterActor which will manage the characters current action
- In the UI create a new Text, this time with a CharacterActionAreaText which will display the areas current action
- In the PlayerInput where we previously bound the move input create a new handler for Action and bind it to CharacterActionArea.OnStartAction
The next step is to give the character an Inventory it can store items in.
- Create an empty child in the Character and name it ‘Inventory’, this is where everything related to items will go
- Add a ListedInventory and assign it to the characters Inventory field. Assign GSItems to its Items field which will be important for persistence later.
- In the UI create a new Text, this time with a ListedInventoryText which will display the items in our inventory
Finally let’s create the pickups that we can place around the environment.
- Create a cube with a trigger collider similar to the ones used for the trigger areas previously
- Add a PickupAction component
- Add a SuspendMovement instruction so the character can’t be moved while performing the action
- Set the CharacterTriggerName to ‘Pickup’ which is the name of the parameter in the GSRobo Animator
- In Items assign the GSItem and set Quantity to 1
- Once you’re happy with the Pickup clone it a couple of times so you can test adding multiple items to the inventory
You should now be able to see ‘Pick up Item’ when you move the character over a pickup. If you start the action using E or A on a Gamepad the character should perform a little animation, the pickup should be destroyed and the item added to the characters inventory.
The Pickup we have created in the last chapter performs an animation on the character only. Here we will create a new action that plays synced animations on both the character and an object in the environment. We’ll also define an item cost so we can actually do something with the items we’ve picked up in the last chapter, this is fairly common behavior for things like keys.
- Drag the GSGate prefab from the assets into the scene
- Create a new child object under GSGate and name it ‘Action’
- Position it in the same place and rotation you want the character to stand when performing the action, for example 1.4 in Z and rotated 180 around Y
- Add an ObjectAction component
- Add a SuspendMovement instruction
- Set the following:
Name: ‘Open’ (displayed by the Action Area)
Cost: 1 x GSItem (can’t start action unless character has the item)
CharacterTriggerName: ‘Open’ (can be checked in the GSRobo Animator)
ObjectCharacterTarget: the action itself (character is moved here)
ObjectTriggerName: ‘Open’ (like in the GSGate animator)
ObjectStateName: ‘Opened’ (see GSGate animator, used in persistence later) ObjectAnimator: GSGate
- Add a BoxCollider with IsTrigger and Size 2 so the action area can collide with and start the object action
If you want to test out the action without having to pick up an item first you can either remove the cost or add some items to the ListedInventory in the editor before you start the game. To see how the animations are synchronized check out the animation events on the Open animations. The START and END events are needed so the action knows its state, the ACT message in the character animation starts the object animation.
In this chapter we’ll give the character a health resource.
- As a child of Status create a new object named ‘Health’ with a ResourceValue component
- Set the type to GSHealth which identifies the resource and gives it a name
- Set Maximum and Value to 100 so the character starts with full health
- In Status add a ResourcePool component
- Add the health resource value to its values
- Assign the pool to the characters Resource Pool field
- In the UI create a new Text with a ResourcePoolText which will display all resources of the Pool and their values
Next we’ll create some areas that deal damage which can subtract from the health resource or add to it.
- Create two new areas, probably easiest to just copy the slow areas and remove the trigger area
- Add TriggerDamagerSender components to both
- In the Damages field assign GSHealthDamage in one and GSHealDamage in the other with a value of 1
- Adjust the timing settings, for example check SendTick and set TickRate to 0.1
- On the character add a TriggerDamageReceiver and assign the character so it can actually receive the damage
The characters health should be reduced when moving into one area and increased in the other.
The maximum Health was previously just statically assigned in the resource itself. In this chapter we’ll add a Vitality attribute and a HealthMaximum stat(which is just VIT*10) which will be assigned as the Health resource maximum.
- In Status add an AttributePool component and assign it to the character
- Add GSVitality to the Attributes and give it a starting value of 10
- Add a new ResourceMaximum with the Health resource value and the GSHealthMaximum stat
- In the UI create a new Text with a AttributePoolText which will display all attributes of the Pool and their values
- Create two new areas by copying the slow areas but this time leave the trigger area and just empty the event handlers and instructions
- Add a new handler to CharacterAdded and drag in the GSVitality attribute
- Choose the AttributeType.Add method in one area and Remove in the other
When you move into the areas you should now receive or loose one point in the Vitality attribute which changes health max by 10.
Finally we will take care of persisting the state of all the things we’ve created this far.
- Create an empty gameobject named ‘Persistence’
- Add a PersistenceContainer component, you’ll find some useful buttons in its inspector like ‘Delete Data’ which can be used to reset its save data
- Set the Key to ‘GS’
- Add GSPersistence to its Areas
- Add a PlayerPrefSaver and assign it as the containers saver
- Add a PersistenceContainer component, you’ll find some useful buttons in its inspector like ‘Delete Data’ which can be used to reset its save data
- Add a ManualPersister to the character and assign it to
- GenericCharacter, CharacterControllerMovement
- Resource-, Attribute- and EffectPool
- Add and assign a ManualPersister to the ObjectAction in the Gate
- The PickupAction has its persister built in so you can assign PersistenceArea and Key directly to it instead of adding a ManualPersister
- In every ManualPersister(and PickupAction) assign GSPersistence as the Area
- Give every ManualPersister(and PickupAction) a Key to identify the piece of data
Instead of manually choosing keys you can also click ‘Generate Missing Keys’ on the persistence container which will generate keys if they are empty.
Since the container should be set to AutoSave and AutoLoad by default everything you do in play mode should now be saved. When you stop play mode and start it again the state will be loaded. You can reset the state by clearing player prefs or deleting data from the container. If you want suspend persistence and start from scratch every time you can unassign the Saver on the container.
permalink: /manual/character title: “Character” sidebar: title: “Manual” nav: manual —
Characters are the brains of the operation for any entity in the world be it player or NPC. They tie all the other concepts together and implement any logic the belongs to a specific character but does not fit any of the other concepts.(input for players, AI for enemies)
CharacterBase has explicit fields for AttributePool and ResourcePool since these probably wont be inherited from. The actor, movement and inventory implementation can be freely defined when inheriting. CharacterBaseTyped/AnimatedCharacterBase define the fields for theses in a generic manner which makes them convenient to inherit from.
The character base implements a very simple messaging channel that can be used for events that may be used by every other system on the character. Typically messages are received from things like animators or timelines and then used by the current action(animation end>action end) or something like the AudioManager(play sound on animation foot down).
A CharacterInstruction defines some type of state change for a character in a way that can be set and reset. This is done to deal with situation where multiple systems modify the same properties without knowing about each other. For example modifications to the movement speed or the visibility of an item slot. AdventureCore defines some useful general instructions like multipliers for attributes or stats and suspending damages, collisions or movement. See the header of the different instructions for a more detailed explanation of each one.
CharacterInstruction(or CharacterInstruction) can be used as an inspector field despite being abstract because AdventureCore comes with a PropertyDrawer that allows choosing the actual implementation in the inspector.
- SoulsCharacterBase provides some common defense, stagger, guard and parry logic
- SoulsPlayerCharacter manages input(movement, lock-on, menu, …), XP recovery and character reset(bonfire)
- SoulsMorningstar character is a specific boss enemy that chooses its attacks based on the position of the player.
- SoulsNonPlayerCharacter base that provides some default behavior for simple NPC characters
- SoulsEnemyCharacter attacks when something enters its trigger area
- SoulsFriendCharacter has some additional objects(talk, trade) when idle and only attacks when damaged
permalink: /manual/acting title: “Acting” sidebar: title: “Manual” nav: manual —
An action is basically anything a character can do that may occupy it for a while.
Where the action behavior is defined depends on its use. It could be defined directly on a character so it can be triggered by AI or input(attacks, moves, …). Alternatively it may also sit on some object in the environment and become available to the character by colliding with a CharacterActionArea(Door, Levers, Dialog, …).
The most important methods to override when implementing a new action are:
CanStart returns if all requirements to start are met(key for door, stamina for move).
OnStart is called by the actor when the action is actually started so this is the main point where things are kicked of.
CanEnd defines if an action can be ended by the outside even though it has not ended itself yet.
OnEnd is called by the actor when the action is ended. This is where things are reset for reusable actions or when actions are destroyed if they are one time.
OnMessage is where messages from the character will be directed when the action is active. For example events from the animator that are needed so the action knows when it has ended.
A character actors manages a stream of actions for a character. That means taking care of how and when they are started and ended as well as forwarding the appropriate messages to them.
CharacterActorBase can technically accommodate more than one active action at a time but the most common scenario is that actions are performed one after the other rather than parallel. SerialCharacterActor serves as a good base class for these scenarios. Both the MinimalCharacterActor(ai) and BufferedCharacterActor(player) inherit from it.
Attacks in AdventureSouls have a custom implementation called SoulsAttackAction.
Attacks CanStart when the character has enough stamina and either the action has not started or when it has and it CanCombo. OnStart it triggers an animation and adds an instruction that suspends the characters regular movement so that only the root motion from the animation is used. It also removes the used stamina from the character.
It responds to the following in OnMessage
START confirms that the animation has actually started, this makes sure we are not responding to messages from the wrong animation.
DMG_ON/DMG_OFF control whether the damage on the weapon is turned on. COMBO is sent when the animation is ready to transition to the next swing.
END means the animation has ended and the action should end.
CanEnd is true when the attack CanCombo and the next action is that same attack. OnEnd the attack reset some things including the combo counter if the next action is not itself.
The roll in AdventureSouls uses an action from AdventureCore called MotionAction. This action is useful for rolls, jumps and any other moves that are based on a character animation.
CanStart returns true when is has no cost or when the character has any of the demanded resources left. OnStart removes the cost from the character and sets the animation trigger. If AlignCharacter is checked(which is the case for a roll) the motion calls AlignToInput on the characters movement. Without this the player would not be able to change direction when rolling multiple times in a row because movement is always suspended.
CanEnd is true when RELEASE has been received by OnMessage so we can transition into the next roll before it has ended. OnEnd just resets things.
The gate in AdventureSouls uses an action from AdventureCore called ObjectAction. This is a good action for interacting with objects in the world. It supports one animation on an object you’re interacting with and another(optional) one on an object that should move in the end. It is useful for doors, levers and chests.
An ObjectAction CanStart always unless there is a cost item defined the character does not have. OnStart it moves the character into place and triggers its animation. When ACT is received in OnMessage it starts the animation on the lever and in OnEnd it starts the one on the gate.
permalink: /manual/item title: “Item” sidebar: title: “Manual” nav: manual —
Items are a type of ScriptableObject that defines whether an item can be equipped or used and how it acts when it is. Derive from ItemBase and override the appropriate methods to do so and declare fields for relevant for items of that type(for example visuals). For items that do not have any behavior of their own(key items, currency) the GenericItem can be used. ItemBase also defines some basic common properties that most item systems will use like name, image and categories.
An Inventory is where items are stored, mostly in combination with a character. InventoryBase defines some common ways an Inventory may be interacted with like adding, removing or using. AdventureCore comes with a simple implementation called ListedInventory which just stores items in a list without any limitations to item quantity or stack size. To implement a more specialized inventory, for example a re4 style case, just inherit from InventoryBase and implement the needed methods.
One important distinction to make when working with Inventory is between an ItemQuantity and an InventoryItem. While both of these have an Item and a Quantity they are used very differently. An InventoryItem represents an entry in an Inventory and can be used to react to the quantity of an item a character has and whether it is equipped. An ItemQuantity is not bound to any Inventory or Character and can be used to configure amounts of items that are gained or used when performing some action like picking up items or using a key.
The Inventory of a Character also acts as the access point for its ItemSlots.
ItemSlots enable Items to be equipped to and interact with a character. Deriving from ItemSlot and overriding equip and unequip is the recommended way to create a new kind of ItemSlot. Since they are in scene and exist by character ItemSlots can take care of any runtime data the item may produce when equipped(visuals, effects, …).
The SoulsArmorItem defines a prefab for the visual of the armor when it is equipped. The instancing of that prefab is done by the SoulsArmorSlot which also makes sure the bones on the renderer are set and manages the instance(as in destroys it when unequipped).
When armor is equipped it can also modify some stats, adding the modifier is done in the slot but could also be done in the item since it has no side effects. Lastly the armor defines attribute requirements which are checked by the item itself in its CanEquip.
In addition to the slot-item combination that armor uses, a weapon also has a SoulsWeapon behavior which serves as the access point for a weapons actions and damages in the weapons prefab. It can be used to check the damage through the prefab when the item is stored in the inventory and for the actions when it has been instanced by the slot.
The SoulsWeaponSlot allows binding input to its weapons light and heavy actions. It then rebinds these whenever the equipped weapon changes.
permalink: /manual/attribute title: “Attribute” sidebar: title: “Manual” nav: manual —
Attributes are whole number metrics that, roughly speaking, define a characters capabilities(strength, vitality, ….) but can also be used for any other number that should be persisted and does not fit any other concept(xp). Attributes do not define any behavior of their own, instead other things that depend on them will check them when needed. To add an attribute simply create an Adventure/AttributeType from the context menu in your assets.
In contrast to attributes an AttributeStat does not have a value of its own. Is is usually calculated based on one or multiple attributes or even just a constant value that just gets changed by modifiers. How a stat is calculated is defined by in its GetValue method, derive from AttributeStat to define your own calculations. Check the stats already defined in AdventureCore for examples.(MultipliedStat, TieredStat, …)
The AttributePool manages the available attributes and stats and their value in the scene. It is usually attached and assigned to a character. This is also where modifiers can register to change exposed values and where observers can register to be notified of changes of attributes and stats.
The SoulsVitality attribute can be leveled up at bonfires and influences the SoulsHealthMaximum stat which defines how many hit points the player has. On enemies the hit point maximum is set directly without using a stat.
SoulsEndurance is pretty much the same but for SoulsStaminaMaximum.
SoulsStrength can be leveled at the bonfire and influences the SoulsPhysicalAttack which is used by weapons to calculate their damage.
SoulsLevel and SoulsExperience are counters that are used to check if the player can level up and how many times they have done so.
Lastly SoulsFlask defines how many flasks are restored to the player when they sit at a bonfire. There is no way to change it currently but it has been added so increasing it with a pickup or some other action can be implemented later.
The stats SoulsPhysicalDefense and SoulsPoiseMaximum are not influenced by any attribute, they just have some base value and are only increased by modifiers like armor.
permalink: /manual/resource title: “Resource” sidebar: title: “Manual” nav: manual —
Resources are floating point metrics that can change on a frame to frame basis. A resource has a resource type which is mostly used to identify it and a ResourceValue behavior which actually holds and changes the value. The default ResourceValue does not change on its own. For more specialized resource values that change their value over time or by some other means you can inherit from ResourceValue(look at ChangingResourceValue for an example)
Resources are managed by a ResourcePool which can be used to check, change and monitor resource values.
SoulsHealth is a simple ResourceValue that does no change on its own. It is checked after a character receives damage to see if they die.
SoulsEndurance is a custom behavior in AdventureSouls called SoulsStamina. This is similar to a ChangingResourceValue but also subtracts from its value when the player is sprinting.
SoulsPoise is a resource value that defines whether the character gets staggered by a hit. It gets damaged by attacks and then refills on its own.
permalink: /manual/effect title: “Effect” sidebar: title: “Manual” nav: manual —
Effects are temporary behaviors that can be added or removed from a character. Every EffectType scriptable object assets describes an effect and points to a prefab that will be instantiated when the effect is added to a character. These prefabs have some implementation of EffectBase at its root which, through its Pool property, gets access to the character it was added to.
The GenericEffect implementation of EffectBase can be used when an effect is only visual or if the effects logic can be expressed using character instructions. Attribute- and StateEffect modify a characters attributes and stats and can be useful for buffs and de-buffs. DamageEffect periodically sends a damage event to the character and may be used for a burning or poisoned status for example. Lastly the TimeoutEffect is an expanded version of GenericEffect that removes itself after some time instead of having to be removed be some outside logic.
One built in way to add an effect to a character is the EffectResourceValue which adds the effect when its value is full. Another would be using a GenericTriggerArea which has events for when a character enters or leaves its area. Since EffectType has an Add method that takes a character as its parameter it is possible to link it directly in the inspector.
The SoulsPoisoned DamageEffect gets added when the players SoulsPoison EffectResourceValue reaches its maximum. The resource is increased when something deals SoulsPoisoning ResourceDamage to the player. This kind of damage is currently only dealt by the sprout enemy which has a ticking DamageSender and is either reduces by just waiting or by using the SoulsMoss ConsumableItem.
SoulsBuffed is a simple TimeoutEffect buff that doubles the characters strength for a set time and then removes itself which is added by the SoulsBooster consumable item. SoulsSlowed reduces a characters movement speed. Both of theses use character instructions for their actual effect.
permalink: /manual/movement title: “Movement” sidebar: title: “Manual” nav: manual —
The movement is where all the traversal logic for a character is located. Movements will probably vary quite a bit depending on if it is meant for the player or some NPC and also depending on the game. This is why MovementBase defines mostly ways for other components to interact and interfere with movement in some general ways. For example it gives other components a way to slow movement down, suspend it when some animation plays or force it to move somewhere. Any more specialized logic will have to lie in the implementation. Conveniently the generic base classes for characters(CharacterBaseTyped, AnimatedCharacterBase) allow us to declare the type in the class header which lets us use the implementation directly from inside the character or any other component that references the character implementation rather than CharacterBase.
AdventureSouls uses the CharacterControllerMovement for its player, the SoulsPlayerCharacter send all the necessary input to it. The zombie enemies use the NavMeshAgentMovement implementation in combination with NavApproachAction so that they can follow the player around the level. Lastly the Morningstar boss uses ManualMovement since it does not really do any traditional movement. Even if the Morningstar had more movement of its own a nav mesh would not really be necessary. The boss arena is a simple rectangle and there is no way for the boss to leave to as long as it aligns itself to the player and walks it will always get to them.
permalink: /manual/persistence title: “Persistence” sidebar: title: “Manual” nav: manual —
Persistence in AAK is built as simple as possible for the consuming components. Therefore we will start exploring persistence from this side and work our way towards the persistence management.
Most built in components of AAK simply reference a PersisterBase to keep their own persistence logic as slim as possible. That PersisterBase has methods for checking if a value has ever been set, retrieving it, setting it and clearing it out. For example the ObjectAction just calls Set(true) when it is triggered and whenever it is started in the future(scene/game reload) it calls Get
Persisters can come in different forms but there is one called ManualPersister that is meant specifically for the case above where some other component needs to save its state. The persister has a field for a key which is needed to identify its data and one for the area. A persister is convenient to keep persistence logic out of game logic but not necessarily needed, the PickupAction for example just deals with persistence itself without using a separate persister.
Another kind of persister is the DestructionPersister which simply sets a bool when it gets destroyed and destroys its GameObject if that bool has been set in the past. This means you can just add this persister to any destructible object to persist the destruction without any other scripting needed.
A PersistenceArea is a ScriptableObject found in Adventure/PersistenceArea that groups together data that will be saved together. This can be useful to separate smaller data that is saved frequently from larger data that is saved less frequently. The PersistenceArea also has the IsGlobal flag which is meant for data that is saved independent of save slot(settings for example).
The PersistenceContainer is the central singleton manager for persistence that is needed in every scene that uses any kind of persistence. This is where all the persistence data is managed and eventually sent to the saver that actually saves it to a storage medium. As you might have guessed from the above ObjectAction example state persistence in AAK is pushed rather than pulled. This means that state is not collected when a save game is created, instead state is sent to the container whenever it changes and the container decides when it actually saves the state. The container can be set to AutoSave so that is saves at the end of the frame whenever anything has changed, otherwise a Save() has to be called by some outside component like a save button.
In the inspector for PersistenceContainer you can find some useful helper buttons that can be used to deleted data when debugging or to check the keys of the persisters in the scene for duplicates. It can also generate random GUID keys for any persisters with an empty key field. So if you have objects that do not necessarily need a readable key just leave the key in their prefab empty and generate them all before you use the persistence.
As mentioned above the container does not save to disk itself, instead it references a PersistenceSaverBase for that purpose. This is done so that the ‘device-facing’ part of persistence is easily replaceable as I anticipate it might have to be customized depending on the built platform and other factors.
State in AdventureSouls is split into a couple different areas. SoulsSystemPersistence is a global area independent of the save slot that is used for settings like the sound effects volume but also to save a short info structure for every save index that is displayed in the title screen. SoulsPlayerPersistence is just used by the player character, the player gets its own area because it is saved quite frequently. The SoulsPermanentPersistence is used to save permanent world state like collected items or pulled levers. Lastly SoulsTemporaryPersistence is where any state resides that is discarded whenever the player changes the level or sits at a bonfire. It contains the state of the enemies and destructible environment objects.
The PersistenceContainer is part of the SoulsSetup prefab which contains all the necessary setup to run the game. Basically if you create a new scene and add the setup as well as a plane for the player to stand on you can start the game and it’ll work. If you check the container on the setups in the debugging scenes like Scenes/Debugging/SoulsDebuggingGeneral you may notice some overrides. For one the key is overridden so that data saved for that debug scene does not override data from the actual game scenes. Also the saver has been removed for most of them, this completely disables persistence because for most debug scenes a complete reset is more useful. If you want to debug the persistence logic you have to reassign or revert that saver.
permalink: /manual/damage title: “Damage” sidebar: title: “Manual” nav: manual —
Damage in AAK is a multi stage process that lets a sender, receiver and kind of damage interact. In PreDamage the sender and receiver are informed that a damage is about to occur between them, they both have the chance to completely cancel the damage here. During OnDamage each kind of damage that the sender has is packaged into a damage event that gets send to all the participants. This is where a senders and receivers can modify the damage value and where the DamageKind applies whatever effect the damage has. Finally in PostDamage the sender and receiver have a chance to react to the combined changes of the applied damages.
An IDamageSender is any component that causes damage occurences. It can be implemented by any class that sends damage. You can Use the static DamageEvent.Send to send your damages. AAK comes with a TriggerDamageSender that uses that regular Unity trigger messages(OnTriggerEnter, …). By default it only sends damage when a TriggerDamageRecevier first enters but it can also be configured to send damage in intervals or at the end.
AAK also comes with the DestructibleDamageReceiver which destroys(or replaces) its gameobject when it gets damaged. This is ideal for destructible environment object or any other object that needs to get damages but does not warrant a full character.
Both the TriggerDamageSender and the TriggerDamageReceiver forward all their damage calls to the character they are attached to(if any). Therefore if we want to react to any kind of damage done to a character we can conveniently do that in the character implementation. The SuspendSendDamage and SuspendReceiveDamage character instructions hook in here.
Damages can theoreticalls be anything, what exactly a damage does is decided by the DamageKind implementation. The most common kind of damage is removing a resource(HP) from a characters ResourcePool. AAK comes with the ResourceDamage damage kind which does exactly this.(ContextMenu>Aventure/ResourceDamage)
Somewhat more complicated than a lot of other games damage in the souls demo is split into physical and poise damage. Poise damage is generally used to signify the brunt of an attack rather then the pure health malus.
The base character defines a lot of the damage handling in the demo. Parries are checked in PreDamageReceive so that the damage can be cancelled if a parry succeeds. In OnDamageReceive the damage value is reduced by defense or set to 0 when a character is guarding. In PostDamageReceive it checks if the charcter dies or is staggered as a result of the damages applied.
One thing we need to make sure is that a player does not damage itself. We could theoretically just cancel any damage that a player weapon might do to the player in PreDamage but using Layers is far more convenient and performant.
The AdventureSouls demo has various Layers that control which objects can interact with each other. Check the Pyhsics settings to see how exactly they are configured.
The player has a general Player Layer that is used for any kind of logical interaction like entering areas or object interactions. The PlayerBody Layer is used to receive damage and interact with physics objects like derstroyed crates. In the current demo the body, just like the logic, uses a simple capsule collider. To get more exact hitboxes and more interesting phyiscs interactions the PlayerBody is the one that should be changed to have more detail. Lastly PlayerDamage is used on any weapons the player uses and interascts mostly with EnemyBody.
The Enemy has a very similar setup to the player with the Enemy, EnemyBody and EnemyDamage Layers. There is also a NeutralDamage Layer that is used by traps so they can send damage to either character. The Trigger Layer is used for logical area to make sure these do not interact with damages or physics at all.
permalink: /manual/souls-player title: “Souls Player” sidebar: title: “Manual” nav: manual —
The following is an overview of the SoulsPlayer prefab in the AdventureSouls demo.
The main transform contains the following components.
- ManualPersister used to save any state related to the player
- CharacterControllerMovement and the CharacterController it uses
- MovementSaver automatically saves the movement data
- Rigidbody and CapsuleCollider which are used by
- GenericTriggerItem with Key PLY which triggers GenericTriggerAreas in the environment(for example the traps in the dungeon)
- CharacterActionArea which is used by the character to detect and start actions in the environment(doors, chests, …)
This transform gets rotated by the movement to face the direction the player is moving.
Contains the TriggerDamageReceiver that receives damage. It also has a second CapsuleCollider with IsTrigger unchecked which pushes around decorators like smashed crates. The components of the Body GameObject were originally located on the Model but have been moved into a separate GameObject to make model replacement easier.
For the sake of simplicity and performance the collision model for the player only consists of a simple capsule. If a more detailed model is required do the following:
- Move TriggerDamageReceiver and Rigidbody to your Model they use any collider in the players model
- Delete the Body GameObject
- Change the Layer of Model to PlayerBody
- Create detailed colliders in the Armature of the model, skip if your model already comes with colliders
This is where the visuals of the player are located. It contains the Animator along with an AnimatorProxy that passes along events and root motion from the animator to the character.
Some other components of AdventureSouls depend on the players model and have to be reassigned if the model is replaced. The following steps show how the player model could be replaced by the zombie model that is usually used by the enemies.
- Open up the SoulsPlayer prefab
- Drag the Models/Characters/SoulsZombie into Pivot
- Add an Animator and an AnimatorProxy to it
- Assign the SoulsPlayer Controller and SoulsPlayerAvatar Avatar to the Animator
- Assign the SoulsPlayer as the Character for AnimatorProxy
- Completely expand the old and new models
- the original armature will contain Left/RightHand transforms under its Hand.L/R transform, drag those over to the new models Hand.L/R(these are used to instantiate items by the weapon and usable item slots)
- Delete the original Model
- In the ArmorTop/Bottom item slots assign CharacterBody to the now missing Template field, it is needed so the armor moves properly with the character model
- On the SoulsPlayerCharacter script reassign the missing Animator to the one you just created and set SoulsZombie as the Model that is replaced by the ragdoll
- At the bottom of SoulsPlayerCharacter you can empty out the HeadRenderer field, it is used for the character customization sliders but the zombie model does not have the needed blend shapes
When you now open up SoulsDebuggingGeneral you should be able to move around and perform actions with the Zombie. If you start the game through the title you’ll see that things there are mostly unchanged but when you start a game the model is in fact changed.
The process to replace the model may be different depending on the model used but generally, as we have seen above, the following dependencies have to be satisfied.
- animator and model on SoulsPlayerCharacter
- hand transforms and body renderer for the item slots
The Critical GameObject holds the actions and triggers necessary for ripostes and backstabs.
The SoulsCriticalAction is the one performed by the player, it uses a GenericTriggerArea to find potential enemies that may be critted. When it is started it forces the enemy to perform the SoulsCrittedAction which disrupts anything it was doing. This action moves the enemy to the Front(riposte) or Back(backstab) transform in order for the animations to line up.
All the actions a player can perform on its own as well as the actor are found here.
- Sit is not really in use currently but serves as an example of a gesture that is performed until some other input arrives
- Turn is started by the movement when the character makes a sharp turn
- Roll is started by the SoulsPlayerCharacter when dodge is pressed and the input direction is not neutral, in the Starting/Ending events in the inspector you can see that it activates a damage while it is active.(the damage has value 0 so it destroys boxes but does not hurt enemies)
- Dodge is started by the SoulsPlayerCharacter when dodge is pressed and the input direction is neutral
- Jump is directly bound to the Act.Jump input by SoulsPlayerCharacter
- Stagger is started by the SoulsCharacterBase base class that the player has in common with the enemies when the character runs out of poise from being hit
- GuardBreak is started by the SoulsCharacterBase when a character guards and runs out of stamina or it is parried while attacking
- Death is started by SoulsPlayerCharacter when it gets hit and health runs out, notice the SignalReceiver which calls the ResetDeath method
Holds items, item slots and inventory. You can add entries to the Items field on the ListedInventory to make the character start out with certain items which is quite useful for debugging.
- Usable is a special item slot that is just a placeholder for the three slots it has as its children. This lets us bind input and UI to that single item slot. Which one is currently active can be switched through using the F key.
- ArmorTop/Bottom are slots for SoulsArmorItem, they need a reference to the CharacterBody in order to copy the bones over to the armor they instantiate which makes the armor mesh move with the character
- WeaponLeft/Right are slots for SoulsWeaponItem, they have a target transform inside the character models armature which they parent their meshes to, the weapons then move because they are children of the characters hand bone. Also notice that the weapon slots have fists as their fallback item, otherwise the character would have no actions to perform when no weapons are equipped
permalink: /manual/souls-scenes title: “Souls Scenes” sidebar: title: “Manual” nav: manual —
The Title scene is where the player should first arrive, this is the case as long as it is at the first place in the build settings. It is where the player can create new games or load existing ones.
UI for the different save slots that are used to create new games and load them
Dialog that is used to customize and name new characters
the model and camera that are used for the render texture in the UI are its children
Scripts that are common to every scene like persistence and the SoulsCommons singleton
Holds all the 3d models in the background which are purely cosmetic
This scene gets loaded when a player loads a save from the title or when using a teleport action. The SoulsLoading script starts loading the target main scene asynchronously. After that it can be configured to wait for player confirmation by setting a UIDocument or to wait for a playable director if that is set. When those are done it additively loads the Temp scene and activates the loaded Main scene.
In the demo loading is always done pretty much immediately because it uses very little assets. The scene is meant as an example for games that grow large enough to warrant it. If you don’t need it simply use SwitchScene instead of LoadScene from the title screen and empty the loading scene parameter in teleport actions.
As mentioned above the actual levels in the demo are split into a main and a temp scene. This is done to simplify the reset process when the player sits down at a bonfire which reloads the temp scene. Therefore the environment, bonfires, managers, lighting and things like that belong in main while enemies and destructibles should be placed in temp. It may make sense to put bigger assets from the temp scene into main too just so it is part of the loading process.
The dungeon stage, in classic games fashion, starts the player off in a prison cell. After freeing themselves by using a key that has dropped into the cell in the intro they collect some basic equipment while going through the stage and finally defeat a boss which unlocks a teleport to the next stage.
The SoulsDungeon script on the Logic object is meant to hold any custom logic for this scene. It uses the persister assigned to it to check if the intro has been played already or if this is the first time the player enters the dungeon. If so it plays the intro and sets the players reset point to the cell.
The intro is a simple TimelineAction which suspends player control and the HUD using character instructions.
First off the player can pick up the key that dropped into the cell in the intro. The pickup has a PickupAction which can be found by the players CharacterActionArea because of the trigger collider on the same object. The model and particles of the pickup are disabled in the actions started event. The item the player gets when interacting is set in the actions Items field.
The door that the key unlocks has an ObjectAction which has the key set in its Cost field. The action has trigger names set for the character(OpenDoor) and the object(Open) which are set on the respective animators when the action is started. It also has the name of the state(Opened) after the action so it can restore it if the game is reloaded.
In the following corridor the player encounters the first enemies. These will not attack on their own because the trigger area which would aggro them has been disabled and unset on the character. They will still attack when hit however but otherwise they just remain in their idle action.
The ladder at the end of the corridor uses the special SoulsLadderAction which responds to up and down input to move the player up or down the ladder. To know if the character started the action from the bottom or top there are SoulsLadderEnterAction at both ends that lead into the ladder action. The actual movement on the ladder is actually done by the root motion of the characters animation. In addition to hiding the weapons and suspending movement the ladder action also has a SuspendMovementCollision instruction which prevents the character from colliding with the ladder during the action.
At the top of the stairs the player finds a chest which works very similarly to a door. The only differences are that this chest does not have a cost and activates its content, which contains a pickup, when used. The LeatherBottom inside the chest can be equipped to increase defense(reduces damage) and poise(harder to stagger). It does this because these stats have been set on the SoulsArmorItem which acts as an IStatModifier.
Another pickup in the courtyard contains the SoulsFlask item which restores the health resource when used and is refilled when the player rests at a bonfire.
The bonfire in the center of the yard contains a very special action that uses three different playables to enter, sit at and leave the bonfire. The binding of director to the characters animator is done at the start of the action so that the object does not need a direct link to the player. When the enter playable is done the bonfire resets the world and player and shows the level up menu.
The crates on the yard have a DestructibleDamageReceiver which destroy the object when it takes damage and replaces it with the destroyed prefab. When the crate is destroyed the DestructionPersister persists that and when the scene is loaded again after quitting the game it immediately destroys the object to restore the previous state. This is different from the player sitting at a bonfire which clears the persistence area that contains the crate and reloads the scene which makes the crate show up again.
Finally there are two doors at the end of the yard. These can only be opened from one side simply because the trigger collider of the action is on one side of the door.
The end of the western hallway contains several traps which periodically spawn a projectile that contains a damage sender that deals physical and poise damage. If the player were to simply walk into the hallway these would damage them and stop their movement due to the stagger caused by the poise damage. The shield found in the chest has a guard action which, while active, nullifies physical damage and turns poise damage into stamina damage.
The lever in the northern hallways, just like doors and chests, uses an ObjectAction. The only difference is that it incorporates an additional animator(Gate) which gets triggered at the end of the action.
The western hallways contain two more zombies which do have their trigger areas set up and will attack once the player gets too close. One detail to note here is that these two carry sword despite enemies not really having an inventory. The swords have just been dragged onto their hands in the editor. The layers of the weapon has also been changed to EnemyBody/EnemyDamage. The light or heavy attack action has then been set on the enemy character to make it use that action when attacking.
After using the lever located in the northern hallways the gate has dropped and the boss arena is accessible.
There is a box collider located a bit into the area that start the fight by calling StartFight on the SoulsBossArena script which does a couple things.
First off it sets the player movements position to outside the arena and suspends persistence completely. This makes it so that quitting and reloading the game during the fight resets the player to outside the arena
- Boss Character
Sets the target to the player and starts the configured action
Shows the big boss HP bar
Activates the fog at the gate so the player can’t leave during the fight
The boss character itself has a couple attacks which are all defined by timelines. Which ones it uses depends on the players location and each one uses a certain amount of energy. When the energy is used up the boss uses the wait action which gives the player a chance to deal damage.
When the boss is defeated the boss area resumes persistence and sets a flag so the state can be restored later. It also activates the teleporter that can be used to move to the shrine stage.
There are two friendly NPCs at the bottom of the shrine stage. What each one does is defined in the action in its Interaction object.
SoulsTalker has a SoulsTalkAction which will display the configured lines of dialog in the general message box when used.
SoulsTrader has a SoulsTradeAction that opens the trading dialog that lets the player exchange experience for the items configured there. This action uses a different persister than the main character which has the permanent persistence area set. Therefore when the player sits at a bonfire the NPC will reset but items that have been bought will still be missing.
The trader sells moss which can be used to reduce the poison resource and boosters which double the characters strength.
On the first level the player encounters a couple sprout enemies. These will approach the player and attack by activating a spherical damage sender that deals poisoning damage. Just like fists or swords this attack still uses a SoulsAttackAction but instead of setting a weapon that deals the damage the activation of the damage and particles is done by creating events for the DMG_ON and DMG_OFF message in the inspector. Another difference from enemies previously encountered is that these have GoHome as their idle action which will make them return to their home if they loose aggro. When they have arrived at their home point they will continue with the action configured in Next so this is where idle poses and such can go.
The second level holds a single zombie enemy which has a PatrolAction as its idle. This action makes it move between the configured transforms. Which one it is currently moving towards is persisted so it will keep its current path even when the game is reloaded.
The top of the shrine stage lets the player activate an elevator to the bottom by walking on a pressure plate.
The main script at work here is AnimationToggler which is used to toggle an animator between two states and persist which one it is at. The current position of the elevator is persisted to Shrine_Elevator_Down.
When the elevator starts moving it activates a CharacterCarrierArea which reparents the player to itself and suspends its movement persistence. It also writes to Shrine_Elevator_Enabled which deactivates the NPCs at the bottom and activates the ones on the top on the next reload.
The NPCs at the Top use the same persistence key as the ones at the bottom so their values will carry over. The only exception is movement which uses a different persister on the ones at the top so the position does no carry over.
Finally the top also contains a teleported that leads back to the dungeon stage. This teleporter has TeleportTarget set so the player will be moved to that transform in the dungeon stage.