1
0
forked from cgvr/DeltaVR
Files
DeltaVR3DModelGeneration/Assets/_PROJECT/Scripts/ModeGeneration/NPCs/NPCController.cs

195 lines
5.8 KiB
C#

using FMOD.Studio;
using System.IO;
using System.Linq;
using UnityEngine;
public abstract class NPCController : MonoBehaviour
{
[Header("Movement Config")]
public Transform mouth;
public float turnSpeed = 5f;
[Header("Voiceline Amplitude Timeline Config")]
public string voicelinesFolder = "CharacterVoicelines";
public string characterSpecificFolder;
public string[] voiceLineKeys;
[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
public bool inverted = false;
protected Transform playerTransform;
private float[] rmsCurve;
private EventInstance currentVoicelineEvent;
private bool isSpeaking;
private float smoothed;
// If you change RMS window in Python, update this
private const float FRAME_DURATION = 0.02f;
private float sampleRate = 1f / FRAME_DURATION; // RMS samples per second (20ms windows)
// Start is called before the first frame update
void Awake()
{
}
void Start()
{
}
// Update is called once per frame
void Update()
{
if (playerTransform != null)
{
// Keep target on same Y as this object (no pitch/roll), so we only yaw.
Vector3 lookTargetPos = new Vector3(playerTransform.position.x, transform.position.y, playerTransform.position.z);
// Direction from this object to the target (on horizontal plane)
Vector3 toTarget = (lookTargetPos - transform.position);
if (toTarget.sqrMagnitude > 0.0001f) // avoid zero-length
{
Quaternion targetRot = Quaternion.LookRotation(toTarget.normalized);
// Interpolate a little each frame towards the target rotation.
float t = Mathf.Clamp01(Time.deltaTime * turnSpeed);
transform.rotation = Quaternion.Lerp(transform.rotation, targetRot, t);
}
}
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;
// Normal mapping: louder -> larger scale
float mapped = Mathf.Clamp(minScaleY + amp, minScaleY, maxScaleY);
// Inverted mapping: louder -> smaller scale
// Achieve this by mirroring within [min, max]:
// invertedTarget = min + (max - (min + amp)) = min + (max - min) - amp
// equivalently:
float invertedTarget = Mathf.Clamp(minScaleY + (maxScaleY - minScaleY) - (mapped - minScaleY), minScaleY, maxScaleY);
float targetY = inverted ? invertedTarget : mapped;
// attack/release smoothing
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 StopSpeaking()
{
isSpeaking = false;
smoothed = inverted ? maxScaleY : minScaleY;
if (mouth != null)
{
var scale = mouth.localScale;
scale.y = smoothed;
mouth.localScale = scale;
}
Debug.Log("mouth scale stopped: " + smoothed);
currentVoicelineEvent.release();
}
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
currentVoicelineEvent = AudioManager.Instance.PlayDialogue(characterSpecificFolder + "/" + key, gameObject);
if (!currentVoicelineEvent.isValid())
{
Debug.LogError("Failed to start dialogue event.");
return;
}
isSpeaking = true;
// Stop mouth on end
float voicelineDuration = rmsCurve.Length * FRAME_DURATION;
Invoke(nameof(StopSpeaking), voicelineDuration + 0.1f);
}
// ---------------------------
// Load RMS Timeline (.txt)
// ---------------------------
private void LoadCurve(string key)
{
string folderPath = Path.Combine(Application.streamingAssetsPath, voicelinesFolder, characterSpecificFolder);
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();
}
}