The open-source Unity framework powering every interaction in Eroticissima. A complete system for building consent-first, gender-free, multiplayer intimate experiences in VR, available now to any developer who wants to build something the industry hasn't bothered to make.
A complete pipeline for building intimate interactions between avatars in Unity, from authoring a motion sequence to delivering it across a multiplayer network with consent confirmed at every step.
The consent handshake between players. VRIK blending that makes avatars feel inhabited. Timeline sequencing for cinematic encounters. NPC partner AI running through the same consent pipeline as a human player.
What makes it different is not the architecture, it is what the architecture refuses to allow. No interaction can be forced. No avatar touched without agreement. Enforced at the network layer before any animation plays.
Two real players. Custom non-binary avatars. A private room. A shared LoveTrigger library they build together.
The SDK handles networking, consent, avatar embodiment, and motion playback. You design the world and the interactions.
Solo experiences where NPC partners respond through the same consent pipeline as human players, with configurable hesitation arc, relationship state that evolves, and audio banks keyed to ConsentType.
Even AlwaysAccept runs through the gate.
Author your own motion library using LoveTriggerSO ScriptableObjects. Bring clips into Unity, attach metadata, and the SDK handles delivery.
Collections can be sold through the marketplace under your own IP.
| Unity Version | 2023 LTS |
| FinalIK (VRIK) | Required for VR Asset Store · v2.x |
| Photon Fusion 2 | Required Photon Dashboard · v2.x |
| Cinemachine | Free Package Manager · v2.x |
| Input System | Free Package Manager · v1.7+ |
| XR Management | Optional Package Manager · v4.x |
| Desktop (Win/Mac/Linux) | HDRP Supported |
| PC VR (SteamVR / OpenXR) | HDRP Supported |
| Standalone VR (Quest / Pico) | URP Supported |
| Mobile | Not Supported |
| Console | No VR |
| Licence | GNU GPL v3 · Free |
Premature yield break made IsInitialized = true unreachable. The system never fully initialized. Fixed by removing the premature yield break so the coroutine reaches completion.
// WRONG — yield break kills the coroutine yield break; // IsInitialized = true was unreachable // CORRECT — coroutine reaches completion IsInitialized = true; OnSystemInitialized?.Invoke();
Two coexisting platform detection systems with diverging enums. Merged into single authoritative PlatformManager with unified Platform enum.
public enum Platform { Desktop, XR, Console, Mobile }
public static class PlatformManager {
public static Platform Current { get; }
}
Server-side path contained // TODO: actual consent logic. Consent structs existed but were not wired. Full async consent handshake now implemented and enforced at the network layer.
private async Task<bool> RequestConsentAsync(
ulong targetPlayerRef,
LoveTriggerRequest request,
float timeoutSeconds = 15f) {
// 1. Send ConsentRequestData RPC to target
// 2. Await ConsentResponseData with timeout
// 3. Return response.Accepted
// && response.RequestID == request.NetworkRequestID
}
Reflection-based patch over private UI methods eliminated. SetupUIElements() is now protected virtual — subclasses override cleanly without reflection.
// Now protected virtual — override in subclasses
protected virtual void SetupUIElements() { }
// InteractableObjectFix.cs deleted entirely
animatorClip marked [Obsolete]. AnimationData singleAnimation is now canonical. Legacy field retained for backwards compatibility only.
[HideInInspector]
[System.Obsolete("Use singleAnimation instead.")]
public AnimationClip animatorClip;
// Always use:
public AnimationData singleAnimation;
namespace LTSystem.Core { }
namespace LTSystem.Interaction { }
namespace LTSystem.Animation { }
namespace LTSystem.Network { }
/// <summary>XML doc on all classes and public methods</summary>
[Header("Section Name")] // on all serialized field groups
Debug.Log("[ClassName] context: " + variable);
// Events
public UnityEvent<LoveTriggerRequest> OnTriggerExecuted; // Inspector
public System.Action OnAnimationComplete; // Code-side
// Coroutine management
private Coroutine _currentSequence;
_currentSequence = StartCoroutine(PlayEncounter());
if (_currentSequence != null) { StopCoroutine(_currentSequence); _currentSequence = null; }
Assets/LoveTriggerSDK/ into your Unity project's Assets/ directory.git clone https://github.com/eroticissima/lovetriggersdk // Copy Assets/LoveTriggerSDK/ → YourProject/Assets/LoveTriggerSDK/
Eroticissima > LoveTriggerSDK > Validate SDK Setup. Fix all warnings before proceeding.Eroticissima > LoveTriggerSDK > Create New LoveTrigger. Fill in the Inspector.triggerID = "lt_kiss_soft" // unique string identifier
displayName = "Soft Kiss" // shown in the selection UI
singleAnimation // AnimationData — your clip + method + blend timing
consentType = ConsentType.Manual // for multiplayer
= ConsentType.AlwaysAccept // for solo / NPC
requiresConsent = true // always, unless explicitly solo
Assets/Resources/LoveTriggers/. The database auto-loads on boot. No manual registration required.InteractableObject to any scene GameObject. Assign your SO to Available Triggers. The full state machine runs automatically.// Or drive it in code:
var trigger = LoveTriggerDatabase.Instance.Get("lt_kiss_soft");
interactableObject.InitiateTrigger(trigger, initiatorGO, partnerGO);
BlendVRIKIn must always live inside a finally{} block. This guarantees VRIK is restored even if the sequence is interrupted — by the player, by a network disconnect, by any error.IEnumerator BlendVRIKOut(VRIK vrik, float duration) {
float elapsed = 0f;
float start = vrik.solver.IKPositionWeight;
while (elapsed < duration) {
elapsed += Time.deltaTime;
vrik.solver.IKPositionWeight =
Mathf.Lerp(start, 0f, elapsed / duration);
yield return null;
}
vrik.solver.IKPositionWeight = 0f;
}
IEnumerator BlendVRIKIn(
VRIK vrik, float duration, float targetWeight = 1f) {
float elapsed = 0f;
float start = vrik.solver.IKPositionWeight;
while (elapsed < duration) {
elapsed += Time.deltaTime;
vrik.solver.IKPositionWeight =
Mathf.Lerp(start, targetWeight, elapsed / duration);
yield return null;
}
vrik.solver.IKPositionWeight = targetWeight;
}
// USAGE — always try/finally
private IEnumerator PlaySequence(VRIK vrik) {
try {
yield return BlendVRIKOut(vrik, 0.4f);
// your animation or Timeline sequence here
}
finally {
// runs even on interruption — no exceptions
yield return BlendVRIKIn(vrik, 0.4f);
}
}
"Source" and "Target". The controller binds the correct avatars at runtime. No hardcoded scene references in your Timeline asset.private void BindCharacterTracks(
PlayableDirector director,
GameObject source, GameObject target) {
foreach (var binding in director.playableAsset.outputs) {
if (binding.streamName.Contains("Source"))
director.SetGenericBinding(binding.sourceObject, source);
if (binding.streamName.Contains("Target"))
director.SetGenericBinding(binding.sourceObject, target);
}
}
finally{}.// Spawn encounter VCam with higher priority
spawnedVCam = Instantiate(trigger.vcamPrefab, transform)
.GetComponent<CinemachineVirtualCamera>();
spawnedVCam.Priority = trigger.vcamPriority; // 15
// In finally{} — gameplay VCam regains control automatically
Destroy(spawnedVCam.gameObject);
[Header("Consent Behavior")]
ConsentType _consentBehavior = ConsentType.Manual;
[Range(0f, 1f)]
float _agreeProbability = 0.85f; // for Manual type
// Hesitation arc — lerped by relationship score
float _thinkDurationMin = 0.5f;
float _thinkDurationMax = 2.0f;
// Relationship score 0–100
// +10 per completed encounter / shorter hesitation at higher scores
float _relationshipScore = 0f;
// Audio banks — keyed to ConsentType, not random
AudioClip[] _greetingClips;
AudioClip[] _agreementClips;
AudioClip[] _declineClips;
Think() → Agree() → ApproachInitiator() → Execute(). Same state machine. No bypass.// NPC subscribes to consent requests automatically on Awake()
LoveTriggerEventBus.OnConsentRequestReceived
+= HandleConsentRequest;
// Response published through the same event bus
LoveTriggerEventBus.RaiseConsentResponseReceived(
new ConsentResponseData {
RequestID = requestData.RequestID,
Accepted = accepted,
ResponderPlayerRef = config.networkPlayerRef
});
Everything you need to start building is available right now. What you build with it is yours.
Sugar Creators get direct technical onboarding from Miyö · €10/month
→ patreon.com/eroticissima