added yarn spinner package

This commit is contained in:
2026-08-14 13:27:17 +02:00
parent c140236bf7
commit c483bda3ea
573 changed files with 81459 additions and 0 deletions
@@ -0,0 +1,63 @@
using UnityEngine;
using UnityEngine.UI;
#nullable enable
namespace Yarn.Unity.Addons.Snaaake
{
/// <summary>
/// Randomly varies the <see cref="Image.fillAmount"/> property of an <see
/// cref="Image"/> over time, at a fixed frame rate.
/// </summary>
public class RandomImageFill : MonoBehaviour
{
/// <summary>
/// The minimum amount to fill.
/// </summary>
public float MinFill = 0.5f;
/// <summary>
/// The maximum amount to fill.
/// </summary>
public float MaxFill = 1.0f;
/// <summary>
/// The degree to which the fill amount will change each frame.
/// </summary>
public float Speed = 1f;
/// <summary>
/// The rate at which the fill amount will change.
/// </summary>
public float FrameRate = 8f;
/// <summary>
/// The image to change the fill amount of.
/// </summary>
public Image? Image;
void Update()
{
if (Image == null)
{
// No image, so nothing to update.
return;
}
var t = Time.time;
// Snap time to framerate
t *= FrameRate;
t = Mathf.FloorToInt(t);
t /= FrameRate;
// Increase speed
t *= Speed;
// Evaluate the noise at our calculated point and update the image's
// fill amount
var noise = Mathf.PerlinNoise1D(t);
var fill = Mathf.Lerp(MinFill, MaxFill, noise);
Image.fillAmount = fill;
}
}
}
@@ -0,0 +1,9 @@
fileFormatVersion: 2
guid: d110ca3d7bebb426d8cb287ecaac4c99
AssetOrigin:
serializedVersion: 1
productId: 267061
packageName: Yarn Spinner+ for Unity, the friendly dialogue and narrative tool
packageVersion: 3.2.8
assetPath: Packages/dev.yarnspinner.unity.addons.snaaake/Runtime/Scripts/RandomImageFill.cs
uploadId: 968678
@@ -0,0 +1,53 @@
using System.Threading;
using System.Collections.Generic;
using UnityEngine;
using Yarn;
using Yarn.Unity;
#nullable enable
namespace Yarn.Unity.Addons.Snaaake
{
/// <summary>
/// A basic dialogue presenter that shows the line text in a TextMeshPro
/// text view.
/// </summary>
/// <remarks>
/// This presenter waits until the line has been externally cancelled before
/// completing. It expects to be used alongside other presenters that signal
/// that the line has been cancelled, like <see cref="VoiceOverPresenter"/>.
/// </remarks>
public class SubtitlePresenter : DialoguePresenterBase
{
public TMPro.TMP_Text? text;
void Awake()
{
if (text != null)
{
text.enabled = false;
}
}
public override async YarnTask OnDialogueStartedAsync() { }
public override async YarnTask OnDialogueCompleteAsync() { }
public override async YarnTask RunLineAsync(LocalizedLine line, LineCancellationToken token)
{
if (text == null)
{
return;
}
text.enabled = true;
text.text = line.TextWithoutCharacterName.Text;
// Wait until the line has been ended by someone else
await YarnTask.WaitUntilCanceled(token.NextContentToken);
text.enabled = false;
}
}
}
@@ -0,0 +1,9 @@
fileFormatVersion: 2
guid: 040745bbc9c4a429d911037da4556ee9
AssetOrigin:
serializedVersion: 1
productId: 267061
packageName: Yarn Spinner+ for Unity, the friendly dialogue and narrative tool
packageVersion: 3.2.8
assetPath: Packages/dev.yarnspinner.unity.addons.snaaake/Runtime/Scripts/SubtitlePresenter.cs
uploadId: 968678
@@ -0,0 +1,19 @@
using System.Collections.Generic;
using UnityEngine;
namespace Yarn.Unity.Addons.Snaaake
{
/// <summary>
/// Stores a list of sprites associated with a character in the Snaaake
/// dialogue presenter.
/// </summary>
[CreateAssetMenu(menuName = "Yarn Spinner/Snaaake/Talking Head Character")]
public class TalkingHeadCharacter : ScriptableObject
{
/// <summary>
/// The list of sprites for the character. The first sprite will be used
/// as the 'idle' pose, when the character is not talking.
/// </summary>
public List<Sprite> sprites = new();
}
}
@@ -0,0 +1,9 @@
fileFormatVersion: 2
guid: 5d31af3bd6f764e14af50289644aa9ac
AssetOrigin:
serializedVersion: 1
productId: 267061
packageName: Yarn Spinner+ for Unity, the friendly dialogue and narrative tool
packageVersion: 3.2.8
assetPath: Packages/dev.yarnspinner.unity.addons.snaaake/Runtime/Scripts/TalkingHeadCharacter.cs
uploadId: 968678
@@ -0,0 +1,141 @@
using System.Threading.Tasks;
using UnityEngine;
using UnityEngine.UI;
#nullable enable
namespace Yarn.Unity.Addons.Snaaake
{
/// <summary>
/// A view that presents an animated talking head in an <see cref="Image"/>,
/// driven by an <see cref="Animator"/>.
/// </summary>
[ExecuteInEditMode]
public class TalkingHeadCharacterView : MonoBehaviour
{
/// <summary>
/// The <see cref="Image"/> to show the sprite in.
/// </summary>
[SerializeField] Image? image;
/// <summary>
/// The character that this view is presenting.
/// </summary>
private TalkingHeadCharacter? currentCharacter;
[Min(0.001f)]
public float FramesPerSecond = 4;
public bool Talking { get; set; }
void OnValidate()
{
if (this.image == null && TryGetComponent<Image>(out var image))
{
this.image = image;
}
}
public TalkingHeadCharacter? CurrentCharacter
{
get => currentCharacter;
set
{
currentCharacter = value;
if (image == null)
{
return;
}
if (currentCharacter != null && currentCharacter.sprites.Count > 0)
{
image.sprite = currentCharacter.sprites[0];
}
else
{
image.sprite = null;
}
}
}
private int _currentFrame = 0;
private int CurrentFrame
{
get => _currentFrame;
set
{
if (this.currentCharacter == null || this.currentCharacter.sprites.Count == 0)
{
return;
}
_currentFrame = value;
if (this.image == null)
{
return;
}
// Ping-pong the actual index of the sprite to use based on our
// frame count
var i = PingPong(CurrentFrame, currentCharacter.sprites.Count - 1);
var sprite = this.currentCharacter.sprites[i];
this.image.sprite = sprite;
}
}
private static int PingPong(int input, int max)
{
return Mathf.Abs(((input + max) % (max * 2)) - max);
}
void Awake()
{
RunAnimation().Forget();
}
public async YarnTask RunAnimation()
{
var destroyToken = this.destroyCancellationToken;
while (destroyToken.IsCancellationRequested == false)
{
if (!Talking)
{
await YarnTask.Yield();
continue;
}
// Start the talking animation. Stop when we're no longer
// talking AND we've reached the end of a cycle.
try
{
do
{
if (currentCharacter == null || currentCharacter.sprites.Count == 0)
{
// No character to show; it must have changed.
// Stop looping.
break;
}
// Show the next frame, and wait until it's time to
// show the next.
CurrentFrame += 1;
await YarnTask.Delay(System.TimeSpan.FromSeconds(1.0f / FramesPerSecond), destroyToken);
} while (Talking || CurrentFrame % (2 * (currentCharacter.sprites.Count - 1)) != 0);
}
catch (System.OperationCanceledException)
{
// We must have been destroyed, so stop immediately
return;
}
}
}
}
}
@@ -0,0 +1,9 @@
fileFormatVersion: 2
guid: 5189d1ee67a4c4eb8a261f0838a93343
AssetOrigin:
serializedVersion: 1
productId: 267061
packageName: Yarn Spinner+ for Unity, the friendly dialogue and narrative tool
packageVersion: 3.2.8
assetPath: Packages/dev.yarnspinner.unity.addons.snaaake/Runtime/Scripts/TalkingHeadCharacterView.cs
uploadId: 968678
@@ -0,0 +1,214 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using UnityEngine;
using Yarn.Unity;
#nullable enable
namespace Yarn.Unity.Addons.Snaaake
{
public class TalkingHeadDialoguePresenter : Yarn.Unity.DialoguePresenterBase
{
[Header("Audio")]
/// <summary>
/// The audio source to monitor.
/// </summary>
public AudioSource? audioSource;
/// <summary>
/// The channel in <see cref="audioSource"/> to monitor.
/// </summary>
public int sampleChannel = 0;
/// <summary>
/// A dictionary for mapping names to animators that present the characters,
/// </summary>
public Yarn.Unity.SerializableDictionary<string, TalkingHeadCharacterView> characterSlots = new();
public Yarn.Unity.SerializableDictionary<string, TalkingHeadCharacter> characters = new();
[Range(0f, 1f)]
public float threshold = 0.5f;
/// <summary>
/// The current animator whose parameters we are changing.
/// </summary>
private TalkingHeadCharacterView? activeView = null;
/// <summary>
/// A buffer containing audio samples collected from <see
/// cref="audioSource"/>.
/// </summary>
/// <remarks>
/// (This is kept in the class because we want to reuse the same buffer,
/// rather than allocate every time we want to sample it.)
/// </remarks>
private float[] sampleBuffer = new float[512];
/// <summary>
/// The amount of time, in milliseconds, between samples of the audio source.
/// </summary>
/// <remarks>
/// We don't need to be super precise with monitoring the audio source, so
/// there's no need to measure every frame. Instead, we'll measure at a fix
/// number of milliseconds between samples.
/// </remarks>
private readonly int timeBetweenSamplesMilliseconds = 50;
/// <summary>
/// The amount of time remaining until we next sample.
/// </summary>
private float timeUntilNextSample = 0;
protected void Start()
{
if (audioSource)
{
// Unity creates the internal buffers and starts recording the first
// time this is called, so call it immediately on start
audioSource.GetOutputData(sampleBuffer, sampleChannel);
}
}
protected void Update()
{
// Ensure that we have the components that we need
if (audioSource == null || activeView == null)
{
return;
}
// Are we due for another audio source sample? If not, wait.
if (timeUntilNextSample > 0)
{
timeUntilNextSample -= Time.deltaTime;
return;
}
// Read sample data out of the audio source.
audioSource.GetOutputData(sampleBuffer, sampleChannel);
// Determine the highest peak level that we hit.
var maxPower = 0f;
for (int i = 0; i < sampleBuffer.Length; i++)
{
maxPower = Mathf.Max(Mathf.Abs(maxPower), sampleBuffer[i]);
}
// Set the parameter to whether or not we have a loud enough signal!
activeView.Talking = maxPower > threshold;
// Reset our clock.
timeUntilNextSample = timeBetweenSamplesMilliseconds / 1000f;
}
/// <summary>
/// Associates a character name with a slot.
/// </summary>
/// <param name="characterName">The character name to associate.</param>
/// <param name="slotName">The name of the slot to associate the character
/// with.</param>
[YarnCommand("set_slot")]
public static void SetCharacterToSlot(string characterName, string slotName)
{
var presenter = FindAnyObjectByType<TalkingHeadDialoguePresenter?>();
if (presenter == null)
{
Debug.LogWarning($"Can't associate {characterName} with slot {slotName}: no {nameof(TalkingHeadDialoguePresenter)}");
return;
}
if (presenter.characters.TryGetValue(characterName, out var characterData) == false)
{
Debug.LogWarning($"Can't associate {characterName} with slot {slotName}: no known character {characterName}");
return;
}
if (presenter.characterSlots.TryGetValue(slotName, out var slot) == false)
{
Debug.LogWarning($"Can't associate {characterName} with slot {slotName}: no slot named {slotName}");
return;
}
slot.CurrentCharacter = characterData;
}
public override async YarnTask RunLineAsync(LocalizedLine line, LineCancellationToken token)
{
// Ensure that we have a character, a slot name for that character, and
// an animator for that slot name.
var characterName = line.CharacterName;
if (characterName == null)
{
Debug.LogWarning($"Can't run talking animation for line {line.TextID} '{line.Text}': line has no character name");
return;
}
if (characters.TryGetValue(characterName, out var character) == false)
{
Debug.LogWarning($"Can't run talking animation for line {line.TextID} '{line.Text}': {characterName} is not a known character");
return;
}
TalkingHeadCharacterView? slot = null;
// Find the slot that contains this character
foreach (var s in characterSlots.Values)
{
if (s.CurrentCharacter == character)
{
slot = s;
break;
}
}
if (slot == null)
{
// We don't know which slot is showing this character
Debug.LogWarning($"Can't run talking animation for line {line.TextID} '{line.Text}': {characterName} is not associated with a slot");
return;
}
// The animator we're working with is this one we've arrived at
activeView = slot;
// Wait until the line is finished
await YarnTask.WaitUntilCanceled(token.NextContentToken);
if (slot != null)
{
// Turn off the animation just in case the audio level is currently
// over the threshold
slot.Talking = false;
}
// Tidy up
activeView = null;
}
public override YarnTask OnDialogueStartedAsync()
{
// Clear all characters from their slots
foreach (var kv in characterSlots)
{
kv.Value.CurrentCharacter = null;
}
return YarnTask.CompletedTask;
}
public override YarnTask OnDialogueCompleteAsync()
{
return YarnTask.CompletedTask;
}
}
}
@@ -0,0 +1,9 @@
fileFormatVersion: 2
guid: b7948f050ae2d45bca291d4149dd41b3
AssetOrigin:
serializedVersion: 1
productId: 267061
packageName: Yarn Spinner+ for Unity, the friendly dialogue and narrative tool
packageVersion: 3.2.8
assetPath: Packages/dev.yarnspinner.unity.addons.snaaake/Runtime/Scripts/TalkingHeadDialoguePresenter.cs
uploadId: 968678