forked from cgvr/DeltaVR
176 lines
4.7 KiB
C#
176 lines
4.7 KiB
C#
using FMOD.Studio;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using UnityEngine;
|
|
|
|
public abstract class NPCController : MonoBehaviour
|
|
{
|
|
protected Transform playerTransform;
|
|
|
|
public string[] voiceLineKeys;
|
|
|
|
|
|
[Header("Mouth Transform")]
|
|
public Transform mouth; // assign your billboard mouth object
|
|
|
|
[Header("Mouth Animation Settings")]
|
|
public float minScaleY = 0.3f;
|
|
public float maxScaleY = 1.0f;
|
|
public float gain = 30f; // multiply RMS to make mouth open larger
|
|
public float attack = 0.6f; // faster opening
|
|
public float release = 0.2f; // slower closing
|
|
|
|
[Header("Timeline Folder (StreamingAssets recommended)")]
|
|
public string timelineFolder = "CharacterVoicelines";
|
|
|
|
private float[] rmsCurve;
|
|
private EventInstance currentVoicelineEvent;
|
|
private bool isSpeaking;
|
|
private float smoothed;
|
|
|
|
private int sampleRate = 50; // RMS samples per second (20ms windows)
|
|
// If you change RMS window in Python, update this
|
|
|
|
|
|
|
|
// Start is called before the first frame update
|
|
void Awake()
|
|
{
|
|
|
|
}
|
|
|
|
void Start()
|
|
{
|
|
|
|
}
|
|
|
|
// Update is called once per frame
|
|
void Update()
|
|
{
|
|
if (playerTransform != null)
|
|
{
|
|
// As if player is on same Y coordinate as this object, to not rotate around Y axis.
|
|
Vector3 lookTargetPos = new Vector3(playerTransform.position.x, transform.position.y, playerTransform.position.z);
|
|
transform.LookAt(lookTargetPos);
|
|
}
|
|
|
|
if (isSpeaking && rmsCurve != null && currentVoicelineEvent.isValid())
|
|
{
|
|
AnimateMouth();
|
|
}
|
|
}
|
|
|
|
private void AnimateMouth()
|
|
{
|
|
|
|
// get FMOD timeline position
|
|
currentVoicelineEvent.getTimelinePosition(out int ms);
|
|
float time = ms / 1000f;
|
|
|
|
// find sample index
|
|
int index = Mathf.FloorToInt(time * sampleRate);
|
|
index = Mathf.Clamp(index, 0, rmsCurve.Length - 1);
|
|
|
|
float amp = rmsCurve[index] * gain;
|
|
|
|
// attack/release smoothing
|
|
float targetY = Mathf.Clamp(minScaleY + amp, minScaleY, maxScaleY);
|
|
if (targetY > smoothed)
|
|
smoothed = Mathf.Lerp(smoothed, targetY, attack);
|
|
else
|
|
smoothed = Mathf.Lerp(smoothed, targetY, release);
|
|
|
|
// apply mouth scale
|
|
Vector3 s = mouth.localScale;
|
|
s.y = smoothed;
|
|
Debug.Log("mouth scale: " + smoothed);
|
|
mouth.localScale = s;
|
|
|
|
}
|
|
|
|
private void OnTriggerEnter(Collider other)
|
|
{
|
|
if (other.gameObject.tag == "Player Head")
|
|
{
|
|
playerTransform = other.transform;
|
|
OnPlayerApproach();
|
|
}
|
|
}
|
|
|
|
protected virtual void OnPlayerApproach() {}
|
|
|
|
private void OnTriggerExit(Collider other)
|
|
{
|
|
if (other.gameObject.tag == "Player Head")
|
|
{
|
|
playerTransform = null;
|
|
OnPlayerLeave();
|
|
}
|
|
}
|
|
|
|
protected virtual void OnPlayerLeave() {}
|
|
|
|
|
|
public void SpeakVoiceLine(int voiceLineId)
|
|
{
|
|
if (voiceLineId < 0 || voiceLineId >= voiceLineKeys.Length)
|
|
{
|
|
Debug.LogError("Invalid voiceLineId!");
|
|
return;
|
|
}
|
|
|
|
string key = voiceLineKeys[voiceLineId];
|
|
|
|
LoadCurve(key); // load RMS data
|
|
Debug.Log("loaded timeline curve");
|
|
|
|
currentVoicelineEvent = AudioManager.Instance.PlayDialogue(key, gameObject);
|
|
|
|
if (!currentVoicelineEvent.isValid())
|
|
{
|
|
Debug.LogError("Failed to start dialogue event.");
|
|
return;
|
|
}
|
|
|
|
//isSpeaking = true;
|
|
|
|
// Stop mouth on end
|
|
currentVoicelineEvent.setCallback((type, inst, param) =>
|
|
{
|
|
if (type == EVENT_CALLBACK_TYPE.STOPPED)
|
|
{
|
|
isSpeaking = false;
|
|
smoothed = minScaleY;
|
|
|
|
if (mouth != null)
|
|
{
|
|
Vector3 s = mouth.localScale;
|
|
s.y = minScaleY;
|
|
mouth.localScale = s;
|
|
}
|
|
}
|
|
return FMOD.RESULT.OK;
|
|
});
|
|
}
|
|
|
|
// ---------------------------
|
|
// Load RMS Timeline (.txt)
|
|
// ---------------------------
|
|
private void LoadCurve(string key)
|
|
{
|
|
string folderPath = Path.Combine(Application.streamingAssetsPath, timelineFolder);
|
|
string filePath = Path.Combine(folderPath, key + ".txt");
|
|
|
|
if (!File.Exists(filePath))
|
|
{
|
|
Debug.LogError("Missing RMS timeline file: " + filePath);
|
|
rmsCurve = null;
|
|
return;
|
|
}
|
|
|
|
var lines = File.ReadAllLines(filePath);
|
|
Debug.Log("read lines: " + lines.Length);
|
|
rmsCurve = lines.Select(l => float.Parse(l, System.Globalization.CultureInfo.InvariantCulture)).ToArray();
|
|
}
|
|
}
|