Concepts

Updated a week ago

MwSkinAdditions Docs

MwSkinAdditions features are implemented through code. The following documentation serves as additional guidance on how each feature works, and how to use them. It is recommended to look at the example implementation for MioHuntress to better understand how to use this library. The example implementation demonstrates how to implement bone transformations, blendshape animations, and voicelines - extra objects are hopefully intuitive following these examples.

Event Subscriber

An event subscriber, internally known as an EventSub, is the fundamental object which enables all the custom behaviour provided by MwSkinAdditions. An EventSub requires a SkinDef and EventSubOptions object through its constructor before it is initialized. Following the creation of an EventSub, its numerous actions can be subscribed to for implementation of custom behaviour in response to certain game events. The GameObject argument provided by the action invocations on an individual EventSub always reference a character with the same SkinDef held by the EventSub - that is to say, it is never necessary to manually check the skin, as the mod already ensures it is correct.

Event Subscriber Options

The EventSubOptions class is a scalable way to specify what unique features of MwSkinAdditions any specific skin should use. It currently contains the following fields:

  • boneTransformations

    • Description: An array of all bone transformations that should be applied to the survivor armature while the skin is active.
    • Type: BoneTransformation[]
    • Default: null
  • extraObjects

    • Description: An array of all additional GameObjects that should be instantiated and parented to the survivor while the skin is active.
    • Type: ExtraObject[]
    • Default: null
  • useAnimations

    • Description: Whether or not the skin should try and apply animations, if there are any. Set to true if any of the following two fields are not null.
    • Type: bool
    • Default: false
  • blinkAnimations

    • Description: An array of blend shape animations that should be applied to the skin, using the blink timing logic provided by the library.
    • Type: BlendShapeAnimation[]
    • Default: null
  • conditionalIdleAnimations

    • Description: An array of idle animations. Each contain an array of blend shape animations, and a boolean function which describes when the animations should and should not be active.
    • Type: IdleAnimation[]
    • Default: null
  • voiceGroups

    • Description: An array of all the different voice groups used by the skin.
    • Type: VoiceGroup[]
    • Default: null
  • transformInCSS

    • Description: Whether or not bone transformations should be applied in the character select screen (CSS). Set to false if drifting occurs in the CSS, but not in-game (e.g. with False Son arms)
    • Type: bool
    • Default: true

Bone Transformations

How do they work?

Bone transformations are a hacky method of maintaining the original look of a mesh without compromising the animations. It works by applying a fixed scale and position offset to specified bones every LateUpdate.

While the Animator is enabled, most bones are given a fixed position, rotation, and scale every Update cycle, according to the current animation. By applying position offsets every LateUpdate (immediately after Update), bones will be moved in time for the framely screen render. Bone positions are then reset to correct positions in the following Animator Update, and the cycle repeats. Position offsets are disabled when the Animator is disabled, as otherwise the position offsets will compound and the bone will begin to drift away. This is not necessary for scaling, as it assigns a fixed value rather than incrementing a value.

How do I use them?

Simply pass an array of BoneTransformation objects into an EventSub via the boneTransformations field of EventSubOptions. The first argument of a BoneTransformation must be an accurate transform path of the survivor's armature, down to a specific bone. The following two required arguments are Vector3s which correspond to the local scale and position offset respectively. You can finally optionally specify another bone transform path which the bone will be positioned relative to, instead of the default of its immediate parent.

Extra Objects

How do they work?

An ExtraObject simply holds a reference to a prefab that is to be instantiated when the skin is applied. The instantiated GameObject is parented to a specified bone of the survivor armature, and can be given an initial scale, position, and rotation. MwSkinAdditions simply handles creating and destroying the ExtraObject - what it does depends entirely on the implementation of the prefab.

How do I use them?

Much like implementing bone transformations, it is as simple as passing an array of ExtraObjects into the extraObjects field of EventSubOptions. The arguments required for the constructor for an ExtraObject are: the prefab to be instantiated, the bone transform path, and the local scale, position, and rotation to set on the instantiated object.

Voicelines

How do they work?

The voicelines system is specially engineered to enable Wwise sound events to play in a manner that is appropriately paced, as to prevent overlapping audio or otherwise excessive sound executions. To achieve this, the system is split up into multiple subcomponents.

  • VoiceInfo

    • A VoiceInfo object represents an individual voiceline. In its constructor, it only requires the sound string, and approximate duration of the sound string, as to prevent potential sound overlaps. It is required that all VoiceInfo objects are created as soon as possible - before the main RoR2 application starts loading - such that they can be added into this library's ContentPack in time.
  • VoiceArray

    • A VoiceArray object represents a group of voicelines for a particular context, e.g. death or getting an item. As such, its constructor takes an array of VoiceInfo objects. Later, each VoiceArray is informed of its assigned VoiceGroup (see below) to appropriately update voiceline timing stopwatches.
  • VoiceGroup

    • Finally, a VoiceGroup object represents a group of VoiceArrays that might play in similar contexts, or at least, within close proximity of each other, e.g. using the secondary, utility, or special skill. Its constructor takes an array of VoiceArray objects, and a minimum and maximum time in which voicelines from any of these contexts may play. For example, if minWait = 6f and maxWait = 12f, then following the execution of a voiceline from any of the VoiceArrays, absolutely none of the other voicelines in the VoiceGroup can play for 6 seconds. The probability that a voiceline plays will then increase leading up to 12 seconds passed, after which the next voiceline with have maximal probability of playing.

How do I use them?

You must first create a soundback of sound events with the current version of Wwise used by Risk of Rain 2. You can watch this bare-minimum tutorial to achieve this. You can then rename the file extension to .sound and include the soundbank in your Thunderstore package, and R2API.Sound will load it for you.

Ensure all VoiceInfo objects are created early, such as in your mod's Awake call. Then in the EventSubOptions object for the EventSub, provide an array of all VoiceGroups into the voiceGroups field. This will ensure the voicelines are playable with the custom logic provided by MwSkinAdditions. Finally, you must implement a delegate to try and play voicelines on the game events provided by the EventSub. This is done by attempting to run GetComponent<VoiceController>() on the provided GameObject argument, and then using one of the many methods to play a sound. For example, To play a random unique sound from a VoiceArray, you can use the method TryPlayRandomUniqueSoundServer, which takes arguments for the VoiceArray, sound source GameObject, and maxProbability to play the sound (from 0-1).

BlendShape Animations

How do they work?

The blendshape animations system provides a way to implement simple linearly-interpolating animations through the blendshapes provided on any mesh, while preventing animation conflicts through a priority system. The existing system was designed specifically with face animations in mind. To this end, blinking logic is already included, and only requires being provided the blendshapes which correspond to blinking.

Each BlendShapeAnimation requires the name of the mesh with the blendshape, the blendshape name, and then a feature ID, priority value, fadeInDuration, holdDuration, fadeOutDuration, and optional flag to indicate if blinking should not occur while the animation is active. A feature ID corresponds to any specific feature such as eyes, eyebrows, mouth, etc. that an animation affects. BlendShapeAnimations check if there is already an animation playing with the same feature ID. If there is, then it cancels the current animation and plays only if it has a higher priority - otherwise, nothing changes.

How do I use them?

Implementing animations in response to events is much the same as with voicelines. After attempting to run GetComponent<ExpressionController>() on the GameObject, the method TryPlayAnimation can be executed with a new BlendShapeAnimation as the argument. This method is to be executed multiple in succession for different features.

To provide a blinking animation, simply provide an array of BlendShapeAnimations to the blinkAnimations field of the EventSubOptions. Instead of the BlendShapeAnimation class, you can provide the animations with the BlinkBlendShapeAnimation class, which contains the recommended arguments for blinking animations. You then only need to provide the mesh name and blendshape name.

You can also implement animations as "idle animations". Every IdleAnimation has a condition which is checked every frame, in the order which they are provided. If its condition is true, then the the ExpressionController attempts to play the animations in the provided animations argument. By default, idle animations are automatically cancelled once the condition becomes false - this can be changed via the final boolean parameter. IdleAnimations can be assigned by passing an array of them into the conditionalIdleAnimations field of the EventSubOptions.