Although the editor, CreationKit, provided plenty of development resources, as “Ixion & Reus” aimed for an innovative gameplay style within Skyrim, there was still the need for a lot of scripting in order to guarantee that everything behaved properly. The next sections depict this level’s major scripting beats: dog commanding mechanics (core mechanics), path blocking feature (dynamic NavMesh change), and customizing Skyrim assets.
To jump straight to a specific topic, click on the buttons below!
COME BACK/RETURN COMMAND
I had to implement the level’s core mechanics from scratch as cooperative gameplay style is not native in Skyrim. I built the commanding mechanics using CreationKit’s AI System (Packages) and its Magic System (Spells and Magic Effects), plus many different scripts (Papyrus Language) to establish the communication between those components and control their effects on the game world. Regarding the technical implementation, I split the quest in two different ‘Quest Scripts’, one that represented the main quest (“Ixion & Reus”), its story beats/ and scripted events and another one that was actually responsible for the level mechanics. Although the two scripts communicated to each other, keeping them separated helped me organizing the project and testing it modularly
The diagram represents the quest mechanics flow:
Quest flow: start (initi) and main mechanics
Game Start: players begin the game in a specific area of the game world, starting up the two Quest Scripts
Init Mechanics: the script responsible for the core mechanics (aaRL_GameMechanics) performs their setup: cleans up the player, changes the character’s race to a child and equips the proper spells to each one of the character’s hands (that’s how he gives commands to Reus). Then, it forces the game to 1st-person perspective, disabling player’s control over the camera – this guarantees players will experience the quest as a child only the moment it starts, without being able to ruin the surprise
Start Main Quest: after the player pulls a lever, the screen fades to black, while the actual main quest script starts, initializing “Ixion & Reus”: the main character, now as Ixion, teleports to his house, a waking up scene begins (removing control from players), and after that sequence, the quest changes the game to 3rd-person perspective, giving control back to players (players still are not able to switch from that view – that was a design decision, so they could experience the quest always looking at the boy and his dog)
Core Mechanics Loop: consists of waiting for players to cast a ‘Spell’. A spell represents a dog command; It changes Reus’ behavior (running a new Package) according to the given order, which might generate a result such as activating an object. The cycle restarts when either the companion finishes its action (successfully finding something, for example), the ‘Spell Effect’ ends (each command has a time limit) the cycle restarts, or players use another command (canceling the previous one)
MainQuest (Stage 1 - Fragment)
The script aaRL_GameMechanics also contains:
Play Scene: called from many other scripts, is responsible for giving feedback through scenes: each scene contains a specific dialogue line according to the value specified – for example, when Reus finds something, Ixion praises it, indicating to players the positive result. The function checks whether the main story is already playing another scene, so it does not overwrite any important moment (story beats have higher priority than feedback).
Dog variables: responsible for defining Reus’ current state. By changing those variables, spells and scripts can update the dog’s package, modifying its behavior accordingly.
In Skyrim, packages are one of the main components in Non-Playable Characters’ AI. A package corresponds to a specific action; a way characters are able to behave. The system evaluates conditions associated to each package in order to update an NPC’s current modus operandi. In “Ixion & Reus”, Reus’ behavior follows that system, using values from afore the mentioned variables as conditions to define the companion’s state. The following state machine describes the dog’s behavior:
State Machine describing Reus' behavior, each circle is an AI state, while arrows are transitions that resulted from player's input or AI behavior
Follow: default behavior; Reus follows Ixion around
Come back: cancels any other behavior; Reus runs fast towards Ixion, returning to its owner
Sniff: searching behavior; Reus wonders around Ixion, pretending that is performing a search action (the actual search happens in the next package, given how packages work in CreationKit – better explained later)
CreationKit's package conditions example: 00RL_DogSearchingPackage contains the 'Sniff' behavior
Follow: the background system performs a ‘mental’ search, looking for a ‘Dog Sniff Point’ (aka ‘Interesting Point’) within a maximum range of 512 units from the player – although in reality, this range should be calculated around the dog, playtest showed that having it around the main character felt more natural and intuitive. If a ‘Dog Sniff Point’ is within that range, the dog moves towards the point and ‘activates’ (interacts with) it
Dog Sniff Points abstract interactive elements which Reus is able to search. They reference actual objects in the game world, establishing a common language between packages and interactive objects. This way, the ‘Sniffing behavior’ only have to identify this type of object. When Reus assumes its ‘Finding behavior’, it moves to ‘Red X’ mark (invisible in game) and triggers the script below, which is responsible for activating the actual (referenced) object
'X Marks are invisible objects in game. This one represents an 'interesting' (Dog Sniff) point
Sniff Range Trigger: a spherical trigger volume around each and every ‘Dog Sniff Point’, this object detects the main character’s presence within its radial range of 512 units, updating the DOG_IN_RANGE variable based on that. The importance of such trigger volume lies in providing more precise feedback for players, explained further.
Pink sphere around the Dog Sniff Point is a trigger volume, representing the main mechanics range
Players control the dog behavior changing its state variables through spells. As previously said, a spell represents a command, thus, there are two spells: one for ordering Reus to ‘go’ (Sniff/Search) and another for come back (Return). Each spell has a scripted effect associated with it, which triggers a sequence of changes, generally: modifying the dog variables to update its state and provide feedback for players.
Players' actions regarding the main mechanics, along with how they work in the background (Spells and their Effects)
If players press the left mouse button, they cast the ‘Sniff Spell’ (search command), triggering sequence of actions shown below:
The code snippet and the diagram describe well the spell effect’s logic. However, the tricky part here is to understand how the dog behavior changes. If players use the search command but are not inside a ‘Sniff Range Trigger’ (or do not enter in one of them within 4 seconds), the script automatically understands that Reus could not find anything – cancelling the effect (the command), giving players a negative feedback, and updating the dog variables (and behavior). That prevented players from waiting for too long without really understanding what was happening.
In Editor, these are the 'Spell' and 'Magic Effect' game components used to implement the mechanics
Players outside the 'Sniff Range Trigger', using the sniff/search command does not work
Players inside the 'Sniff Range Trigger', using the sniff/search command works
On the other hand, if players use that command and are inside a ‘Sniff Range Trigger’ (or enter in one of them within 4 seconds), the DOG_IN_RANGE variable updates to 1 and Reus’ new behavior updates to “finding”, in which the dog perform the actual search, moving to and interacting with the found ‘Dog Sniff Point’. The actual search had to be done in the “finding” behavior, given the strict nature of the editor’s AI system; it is not possible to access packages, or their content, through script – it is only possible to detect when they start/change/end as well as the result of their effects (even not all of them). So, using triggers and scripting, I made sure to meet the conditions for “finding” behavior, giving players some leeway (4 seconds) with the “searching” behavior before Reus’ state actually changed. All of that also gave me the opportunity to provide more granular feedback regarding the different dog states.
Providing feedback: this code belongs to the dog’s script. It detects when Reus change its current behavior (package) and if the new package is the “finding behavior”. If these conditions are met, then it provides a positive feedback for players, indicating that Reus found something, while updating the DOG_FOUND variable, so the effect does not get canceled later by the aaRL_DogSniffEffect script (OnUpdate()).
Similarly, if players press the right mouse button, they are able to call Reus back (Return command):
aaRL_DogReturnEffect: much simpler than the search command, this one cancels out all any other command, setting the respective dog variables to zero. Then, it updates Reus’ behavior with a “return” package, which makes the companion run quickly towards Ixion.
The next video summarizes the core mechanics, demonstrating how they work:
Players use both commands: search and return, receiving proper feedback from each one of them.
In “Ixion & Reus”, a very important element of the puzzles, given their traversal aspect, is to clear up the path for either Ixion or Reus so they can move on together, i.e. pressing a button to rotate a slicer, stepping on a pressure plate to pull down a gate of spears, etc.
PATH BLOCKING FEATURE
However, as I playtested the quest, a new issue came up; given the nature of the AI pathing system, Reus, on its attempt to follow Ixion around, would ignore obstacles, climbing them or even straight up teleporting next to the main character. That happened because AI uses NavMesh to navigate the environment and, trying to move where it was supposed to, did its best to overcome any obstacles on the way – if there was NavMesh next to the followed character, the AI would do its best to get there.
When following another character, an NPC does its best to get close enough, moving to wherever area the NavMesh (red triangles) connects to
Both yellow and green collision volumes work as blockers, dynamically cutting the NavMesh and blocking the dog's path, in order to prevent it from reaching out Ixion
In order to fix that, I had to dynamically cut the NavMesh, preventing NPCs from being able to access certain areas. I used collision volumes (boxes) with specific layer types, L_NAVCUT and L_ACTORZONE, which respectively cut the NavMesh and defined invisible blockers for NPCs only. Moreover, such volumes were linked to invisible objects (‘X Markers’), so the effect of enabling/disabling these markers cascaded to the linked volumes, enabling/disabling them as well.
Then, I incorporated such functionality into the script aaRL_UnlockOnActivate, created with the original purpose of opening/closing doors based on the interaction with objects – example: pulling a lever could either close/lock or open/unlock a nearby gate.
INIT state, where the collision volumes and referenced objects are initialized correctly.
aaRL_UnlockOnActivate.OnActivate(): called both in LOCKED and UNLOCKED states, handles the toggle logic, alternating between the two states.
Basically, whenever the path should be cleared, on top of ‘opening’ an actual object in the game world, the script also enables a referenced marker, bringing back the collision volumes. On the other hand, whenever the path should be blocked, in addition to ‘closing’ an actual object, the script disables such marker, momentarily removing the linked volumes.
State machine describing the general behavior of aaRL_UnlockOnActivate script
aaRL_UnlockOnActivate.LockDoor(): enables the 'XMarker' (blocking the path) and perform close/lock-related tasks.
aaRL_UnlockOnActivate.UnlockDoor(): disables the 'XMarker' (clearing up the path) and perform open/unlock-related tasks.
Combining the collision volumes along with the scripting allowed me to prevent Reus' movement from bugging out
CUSTOMIZING SKYRIM ASSETS
The previous topic brings up another important task: customizing Skyrim assets. Among all the assets which I had to do some sort of scripting for making them behave as the quest needed, pressure plates and poles stand out as the ones that required the biggest work effort.
In Editor, poles and pressure plates, the elements that required the biggest amount of work
Regarding pressure plates, I needed them to be more reliable regarding their activation system; for “Ixion & Reus”, pressure plates should be able to precisely detect when a character stepped in or out of them, as that always changed another puzzle piece (door, poles, slicer, etc.). In their original implementation, if a character stepped in and out often in a short period of time, the puzzle piece would freak out, inverting states and behaving wrongly. On the other side of that system, the poles needed to stay up or down (blocking or not blocking the path) according to the pressure plate’s detection. The diagram illustrates the intended behavior for the pressure plate-pole pair:
Other puzzle pieces – such as slicers and doors – behave the same way as the poles: while either Ixion or Reus are stepping on a plate, the linked poles (or other objects) stay “open”; when there is no one stepping on that plate, the linked objects automatically close.
In order to accomplish that, first I stopped using the pressure plate’s original detection system (in the game, pressure plates detect that a character stepped on through Physics, which works fine if there is no need for fine grained control). I kept the pressure plates only for visual cue, but used a trigger volume around them for the actual detection. Attached to that volume, two scripts work together: aaRL_UnlockOnActivate and aaRL_TrapBaseExtended.
OnUpdate(): I added new functionalities to the aaRL_UnlockOnActivate script, allowing it to work attached to a trigger volume. The script checks whether it is supposed to behave as a pressure plate and, if true, keeps verifying periodically if its current lock/unlock state matches the presence/absence of a character within its boundaries.
VerifyLock(): called by the previous method, this procedure is responsible for checking the Pressure Plate current state against what it should be, and performing the Lock/Unlock tasks in order to keep everything consistent. The 'reversed logic' flag means that the relation between pressure plate and puzzle piece should be reverted (character's presence represents locking state). That addition was necessary for the last puzzle, which required such behavior.
The trigger volume also has an aaRL_TrapBaseExtended script, which is a modified version of Bethesda’s original script TrapTriggerBase. With this new script, I added a Boolean flag ('super reliable'), which, when set, changed the way the trigger object reacted to activation; instead of activating itself, it calls a method to go over every referenced activator and run their “SetOpen()” method.
The red box is a trigger volume that utilizes the mentioned scripts to simulate the pressure plate behavior in a more reliable way
Injected variables: added to auxiliate in the code injection. 'Target Activators' are the poles. 'Super Reliable' sets the use of "ActivateChain()" method
ActivateChain(): the original script activated itself, relying on the poles' original toggle behavior to alternate between states. This method directly sets their state, guaranteeing its consistency
Code injection: using the aforementioned variables and methods, I made this change to every section of the original script that referenced 'activate(self as objectReference)'. Instead of relying on the poles, the script itself sets their state according to the requirement