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

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();
}
}