A reusable library for adding screen reader accessibility to Unity games. Works with MelonLoader, BepInEx, or any Unity mod framework.
- UniversalSpeech integration - P/Invoke wrapper for the UniversalSpeech library with SAPI fallback
- Braille display support - Automatic output to braille displays via screen reader
- High-level speech manager - Duplicate prevention, repeat functionality, speaker formatting
- Text cleaning - Strips Unity rich text tags (
<color>,<size>,<b>, etc.) with extensible custom replacements - Logging abstraction - Integrate with any logging system (MelonLoader, BepInEx, custom)
- Multi-target support - Builds for net6.0, net472, and net35 for broad compatibility with various games
- UniversalSpeech.dll in the game directory
- One of:
- .NET 6.0 (IL2CPP games with MelonLoader 0.6+)
- .NET Framework 4.7.2 (Mono games)
- .NET Framework 3.5 (older Mono games, e.g. Unity 2017)
dotnet add package UnityAccessibilityLibOr add to your .csproj:
<PackageReference Include="UnityAccessibilityLib" Version="2.0.0" />Add the project to your solution and reference it:
<ProjectReference Include="..\UnityAccessibilityLib\UnityAccessibilityLib.csproj" />Build the library and reference the DLL:
<Reference Include="UnityAccessibilityLib">
<HintPath>path\to\UnityAccessibilityLib.dll</HintPath>
</Reference>using MelonLoader;
using UnityAccessibilityLib;
public class MelonLoggerAdapter : IAccessibilityLogger
{
private readonly MelonLogger.Instance _logger;
public MelonLoggerAdapter(MelonLogger.Instance logger)
{
_logger = logger;
}
public void Msg(string message) => _logger.Msg(message);
public void Warning(string message) => _logger.Warning(message);
public void Error(string message) => _logger.Error(message);
}
public class MyAccessibilityMod : MelonMod
{
public override void OnInitializeMelon()
{
AccessibilityLog.Logger = new MelonLoggerAdapter(LoggerInstance);
if (SpeechManager.Initialize())
{
LoggerInstance.Msg("Speech system ready");
}
}
}using BepInEx;
using BepInEx.Logging;
using UnityAccessibilityLib;
public class BepInExLoggerAdapter : IAccessibilityLogger
{
private readonly ManualLogSource _logger;
public BepInExLoggerAdapter(ManualLogSource logger)
{
_logger = logger;
}
public void Msg(string message) => _logger.LogInfo(message);
public void Warning(string message) => _logger.LogWarning(message);
public void Error(string message) => _logger.LogError(message);
}
[BepInPlugin("com.example.mymod", "MyAccessibilityMod", "1.0.0")]
public class MyPlugin : BaseUnityPlugin
{
void Awake()
{
AccessibilityLog.Logger = new BepInExLoggerAdapter(Logger);
if (SpeechManager.Initialize())
{
Logger.LogInfo("Speech system ready");
}
}
}// Dialogue with speaker name
SpeechManager.Output("Phoenix", "Hold it!", TextType.Dialogue);
// Output: "Phoenix: Hold it!"
// Narrator text
SpeechManager.Output(null, "The court fell silent.", TextType.Narrator);
// Output: "The court fell silent."
// Menu/system announcements
SpeechManager.Announce("Save complete", TextType.System);
// Repeat last dialogue (bind to a key)
SpeechManager.RepeatLast();| Method | Description |
|---|---|
Initialize() |
Initialize the speech system. Returns true on success. |
Output(speaker, text, textType) |
Speak text with optional speaker name. |
Announce(text, textType) |
Speak text without a speaker name. |
RepeatLast() |
Repeat the last dialogue/narrator text. |
Stop() |
Stop current speech. |
ClearRepeatBuffer() |
Clear stored repeat text. |
| Property | Description |
|---|---|
DuplicateWindowSeconds |
Time window for duplicate suppression (default: 0.5s) |
EnableLogging |
Whether to log speech output (default: true) |
EnableBraille |
Whether to output to braille displays (default: true) |
FormatTextOverride |
Custom delegate for text formatting (see Extensibility) |
ShouldStoreForRepeatPredicate |
Custom predicate for repeat storage (see Extensibility) |
TextTypeNames |
Dictionary mapping text type IDs to names for logging |
| Value | Description |
|---|---|
Dialogue |
Character dialogue (formatted as "Speaker: text") |
Narrator |
Narrator/descriptive text |
Menu |
Menu item text |
MenuChoice |
Menu selection |
System |
System messages |
CustomBase |
Base value (100) for defining custom text types |
Low-level access to UniversalSpeech:
UniversalSpeechWrapper.Initialize(); // Initialize (called by SpeechManager)
UniversalSpeechWrapper.Speak("text", true); // Speak with interrupt
UniversalSpeechWrapper.DisplayBraille("text"); // Output to braille display
UniversalSpeechWrapper.Stop(); // Stop speech
UniversalSpeechWrapper.IsScreenReaderActive(); // Check if screen reader is runningBasic usage:
string clean = TextCleaner.Clean("<color=#ff0000>Red text</color>");
// Result: "Red text"
string combined = TextCleaner.CombineLines("Line 1", "<b>Line 2</b>", "Line 3");
// Result: "Line 1 Line 2 Line 3"Custom text replacements (applied after tag removal):
// Simple string replacement
TextCleaner.AddReplacement("♥", "heart");
TextCleaner.AddReplacement("→", "arrow");
// Regex replacement
TextCleaner.AddRegexReplacement(@"\[(\d+)\]", "footnote $1");
// Clear custom replacements
TextCleaner.ClearReplacements(); // Clear string replacements only
TextCleaner.ClearRegexReplacements(); // Clear regex replacements only
TextCleaner.ClearAllCustomReplacements(); // Clear all// Set your logger implementation
AccessibilityLog.Logger = new MelonLoggerAdapter(loggerInstance);
// or
AccessibilityLog.Logger = new BepInExLoggerAdapter(loggerInstance);
// Or create a simple console logger for testing
AccessibilityLog.Logger = new ConsoleLogger();
public class ConsoleLogger : IAccessibilityLogger
{
public void Msg(string message) => Console.WriteLine(message);
public void Warning(string message) => Console.WriteLine($"[WARN] {message}");
public void Error(string message) => Console.WriteLine($"[ERROR] {message}");
}The library includes Net35Extensions with polyfills for methods not available in .NET 3.5:
Net35Extensions.IsNullOrWhiteSpace(string)- Use instead ofstring.IsNullOrWhiteSpace
Define custom text types for game-specific content:
public static class MyTextTypes
{
public const int Tutorial = TextType.CustomBase + 1; // 101
public const int Combat = TextType.CustomBase + 2; // 102
public const int Inventory = TextType.CustomBase + 3; // 103
}
// Register names for logging
SpeechManager.TextTypeNames = new Dictionary<int, string>
{
{ TextType.Dialogue, "Dialogue" },
{ TextType.Narrator, "Narrator" },
{ MyTextTypes.Tutorial, "Tutorial" },
{ MyTextTypes.Combat, "Combat" },
};
// Use custom types
SpeechManager.Announce("Press A to jump", MyTextTypes.Tutorial);Override how text is formatted before output:
SpeechManager.FormatTextOverride = (speaker, text, textType) =>
{
// Custom formatting logic
if (textType == MyTextTypes.Combat)
return $"Combat: {text}";
if (!string.IsNullOrEmpty(speaker))
return $"{speaker} says: {text}";
return text;
};Control which text types are stored for repeat functionality:
SpeechManager.ShouldStoreForRepeatPredicate = (textType) =>
{
// Store dialogue, narrator, and tutorial text for repeat
return textType == TextType.Dialogue
|| textType == TextType.Narrator
|| textType == MyTextTypes.Tutorial;
};- Download UniversalSpeech from GitHub
- Copy
UniversalSpeech.dll(32-bit or 64-bit depending on the game's architecture) to the game's root directory - The library will automatically use any active screen reader, or fall back to Windows SAPI
Supported screen readers:
- NVDA
- JAWS
- Window-Eyes
- System Access
- Supernova
- ZoomText
- SAPI (fallback)
If you're upgrading from the old MelonAccessibilityLib package:
- Update your NuGet package reference from
MelonAccessibilityLibtoUnityAccessibilityLib - Update your
usingstatements fromusing MelonAccessibilityLib;tousing UnityAccessibilityLib;
No API changes - all classes and methods remain the same.
MIT License - see LICENSE for details.