Initial Unity project commit

This commit is contained in:
2026-07-08 19:53:15 +08:00
commit 75f254212e
15657 changed files with 11633879 additions and 0 deletions
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 5e5cc7c9e5ec47b42942724bf95b7ac8
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,521 @@
using System;
using System.Collections.Generic;
using UnityEngine;
#if DOTWEEN
using DG.Tweening;
#endif
namespace Test_2022.CameraControl
{
[DisallowMultipleComponent]
public sealed class CameraWaypointSwitcher : MonoBehaviour
{
public enum TransitionMode
{
Instant = 0,
Tween = 1
}
public enum PathMode
{
Loop = 0,
PingPong = 1
}
public enum ShakeMode
{
Off = 0,
Handheld = 1,
WhileMoving = 2
}
[Serializable]
public struct Waypoint
{
public Transform point;
[Min(0f)] public float holdSeconds;
}
[Header("Target")]
[SerializeField] Transform target;
[SerializeField] bool autoStartOnEnable = true;
[SerializeField] bool useUnscaledTime;
[SerializeField] bool restoreOnDisable = true;
[Header("Path")]
[SerializeField] TransitionMode transitionMode = TransitionMode.Instant;
[SerializeField] PathMode pathMode = PathMode.PingPong;
[SerializeField] List<Waypoint> waypoints = new List<Waypoint>();
[SerializeField, Min(0f)] float moveDurationSeconds = 1.2f;
[SerializeField, Min(0f)] float defaultHoldSeconds = 1.5f;
[SerializeField] AnimationCurve moveEase = AnimationCurve.EaseInOut(0f, 0f, 1f, 1f);
[SerializeField] bool startFromNearestWaypoint = true;
[Header("Shake")]
[SerializeField] ShakeMode shakeMode = ShakeMode.Handheld;
[SerializeField] bool createRigForShake = true;
[SerializeField] bool shakePosition = true;
[SerializeField] bool shakeRotation = true;
[SerializeField, Min(0f)] float handheldPositionAmplitude = 0.015f;
[SerializeField, Min(0f)] float handheldRotationAmplitude = 0.45f;
[SerializeField, Min(0f)] float handheldFrequency = 0.35f;
[SerializeField, Min(0f)] float handheldSmoothingSeconds = 0.12f;
[SerializeField] bool handheldUseFixedSeed;
[SerializeField] int handheldSeed = 12345;
[Header("Shake (While Moving)")]
[SerializeField, Min(0f)] float shakeStrengthPosition = 0.02f;
[SerializeField, Min(0f)] float shakeStrengthRotation = 0.25f;
[SerializeField, Min(1)] int shakeVibrato = 8;
[SerializeField, Range(0f, 180f)] float shakeRandomness = 25f;
Vector3 basePosition;
Quaternion baseRotation;
Transform rig;
Transform shakeTransform;
Vector3 shakeBaseLocalPosition;
Quaternion shakeBaseLocalRotation;
Vector3 shakePosOffset;
Vector3 shakeRotOffset;
float shakeSeedX;
float shakeSeedY;
float shakeSeedZ;
float lastShakeTime;
Coroutine cutRoutine;
bool isPlaying;
#if DOTWEEN
Sequence sequence;
Tween shakePosTween;
Tween shakeRotTween;
#endif
public IList<Waypoint> Waypoints => waypoints;
void Reset()
{
var cam = Camera.main;
if (cam != null)
target = cam.transform;
}
void OnEnable()
{
if (autoStartOnEnable)
StartPlayback();
}
void OnDisable()
{
StopPlayback(restoreOnDisable);
}
public void StartPlayback()
{
if (target == null)
return;
if (waypoints == null || waypoints.Count == 0)
return;
StopPlayback(false);
CacheBasePose();
SetupRigAndShakeTransforms();
CacheShakeLocalBase();
ResetHandheldState();
var moveTransform = rig != null ? rig : target;
var indices = BuildTraversalIndices(pathMode, waypoints.Count);
if (indices.Count == 0)
return;
if (startFromNearestWaypoint)
RotateTraversalToNearest(indices, moveTransform.position);
isPlaying = true;
if (transitionMode == TransitionMode.Instant)
{
cutRoutine = StartCoroutine(RunCutLoop(moveTransform, indices));
return;
}
#if DOTWEEN
sequence = DOTween.Sequence();
sequence.SetUpdate(useUnscaledTime);
sequence.SetAutoKill(false);
for (var i = 0; i < indices.Count; i++)
{
var waypointIndex = indices[i];
var wp = waypoints[waypointIndex].point;
if (wp == null)
continue;
var move = moveTransform.DOMove(wp.position, moveDurationSeconds);
move.SetEase(moveEase);
var rot = moveTransform.DORotateQuaternion(wp.rotation, moveDurationSeconds);
rot.SetEase(moveEase);
sequence.Append(move);
sequence.Join(rot);
if (shakeMode == ShakeMode.WhileMoving)
sequence.Join(CreateOneShotShakeTween(moveDurationSeconds));
sequence.AppendInterval(GetHoldSeconds(waypointIndex));
}
sequence.SetLoops(-1, LoopType.Restart);
sequence.Play();
#endif
}
public void StopPlayback(bool restore)
{
isPlaying = false;
if (cutRoutine != null)
{
StopCoroutine(cutRoutine);
cutRoutine = null;
}
#if DOTWEEN
if (sequence != null)
{
sequence.Kill(false);
sequence = null;
}
if (shakePosTween != null)
{
shakePosTween.Kill(false);
shakePosTween = null;
}
if (shakeRotTween != null)
{
shakeRotTween.Kill(false);
shakeRotTween = null;
}
#endif
ResetShakeTransformLocal();
if (restore)
RestoreBasePose();
TeardownRigIfCreated();
}
public void ClearWaypoints()
{
waypoints.Clear();
}
public bool AddWaypoint(Transform point, float holdSeconds = -1f)
{
if (point == null)
return false;
for (var i = 0; i < waypoints.Count; i++)
{
if (waypoints[i].point == point)
return false;
}
waypoints.Add(new Waypoint
{
point = point,
holdSeconds = holdSeconds
});
return true;
}
public int RemoveNullWaypoints()
{
return waypoints.RemoveAll(w => w.point == null);
}
void CacheBasePose()
{
if (target == null)
return;
basePosition = target.position;
baseRotation = target.rotation;
}
void RestoreBasePose()
{
if (target == null)
return;
target.position = basePosition;
target.rotation = baseRotation;
}
void SetupRigAndShakeTransforms()
{
rig = null;
shakeTransform = target;
if (target == null)
return;
if (!createRigForShake || shakeMode == ShakeMode.Off)
return;
var rigGo = new GameObject(target.name + "_Rig");
rigGo.transform.SetPositionAndRotation(target.position, target.rotation);
rigGo.transform.SetParent(target.parent, true);
rig = rigGo.transform;
target.SetParent(rig, true);
shakeTransform = target;
target.localPosition = Vector3.zero;
target.localRotation = Quaternion.identity;
}
void CacheShakeLocalBase()
{
if (shakeTransform == null)
return;
shakeBaseLocalPosition = shakeTransform.localPosition;
shakeBaseLocalRotation = shakeTransform.localRotation;
}
void ResetShakeTransformLocal()
{
if (shakeTransform == null)
return;
shakeTransform.localPosition = shakeBaseLocalPosition;
shakeTransform.localRotation = shakeBaseLocalRotation;
}
void ResetHandheldState()
{
shakePosOffset = Vector3.zero;
shakeRotOffset = Vector3.zero;
lastShakeTime = useUnscaledTime ? Time.unscaledTime : Time.time;
if (handheldUseFixedSeed)
{
shakeSeedX = handheldSeed * 0.0137f + 10.1f;
shakeSeedY = handheldSeed * 0.0213f + 20.2f;
shakeSeedZ = handheldSeed * 0.0341f + 30.3f;
}
else
{
shakeSeedX = UnityEngine.Random.value * 1000f;
shakeSeedY = UnityEngine.Random.value * 1000f;
shakeSeedZ = UnityEngine.Random.value * 1000f;
}
}
void TeardownRigIfCreated()
{
if (rig == null || target == null)
{
rig = null;
shakeTransform = null;
return;
}
if (target.parent == rig)
target.SetParent(rig.parent, true);
if (Application.isPlaying)
Destroy(rig.gameObject);
else
DestroyImmediate(rig.gameObject);
rig = null;
shakeTransform = null;
}
void LateUpdate()
{
if (!isPlaying)
return;
if (shakeMode != ShakeMode.Handheld)
return;
if (shakeTransform == null)
return;
if (!shakePosition && !shakeRotation)
return;
var now = useUnscaledTime ? Time.unscaledTime : Time.time;
var dt = Mathf.Max(0f, now - lastShakeTime);
lastShakeTime = now;
var t = now * Mathf.Max(0f, handheldFrequency);
var nx = Mathf.PerlinNoise(shakeSeedX, t) * 2f - 1f;
var ny = Mathf.PerlinNoise(shakeSeedY, t + 11.1f) * 2f - 1f;
var nz = Mathf.PerlinNoise(shakeSeedZ, t + 22.2f) * 2f - 1f;
var targetPos = Vector3.zero;
if (shakePosition && handheldPositionAmplitude > 0f)
{
targetPos = new Vector3(nx, ny * 0.7f, nz * 0.35f) * handheldPositionAmplitude;
}
var targetRot = Vector3.zero;
if (shakeRotation && handheldRotationAmplitude > 0f)
{
targetRot = new Vector3(ny * 0.75f, nx * 0.5f, nz) * handheldRotationAmplitude;
}
var smooth = handheldSmoothingSeconds <= 0f ? 1f : 1f - Mathf.Exp(-dt / Mathf.Max(0.0001f, handheldSmoothingSeconds));
shakePosOffset = Vector3.Lerp(shakePosOffset, targetPos, smooth);
shakeRotOffset = Vector3.Lerp(shakeRotOffset, targetRot, smooth);
shakeTransform.localPosition = shakeBaseLocalPosition + shakePosOffset;
shakeTransform.localRotation = shakeBaseLocalRotation * Quaternion.Euler(shakeRotOffset);
}
System.Collections.IEnumerator RunCutLoop(Transform moveTransform, List<int> indices)
{
while (true)
{
for (var i = 0; i < indices.Count; i++)
{
var waypointIndex = indices[i];
if (waypointIndex < 0 || waypointIndex >= waypoints.Count)
continue;
var wp = waypoints[waypointIndex].point;
if (wp == null)
continue;
moveTransform.SetPositionAndRotation(wp.position, wp.rotation);
var hold = Mathf.Max(0f, GetHoldSeconds(waypointIndex));
if (hold > 0f)
{
if (useUnscaledTime)
yield return new WaitForSecondsRealtime(hold);
else
yield return new WaitForSeconds(hold);
}
else
yield return null;
}
}
}
float GetHoldSeconds(int waypointIndex)
{
if (waypointIndex < 0 || waypointIndex >= waypoints.Count)
return defaultHoldSeconds;
var h = waypoints[waypointIndex].holdSeconds;
return h > 0f ? h : defaultHoldSeconds;
}
static List<int> BuildTraversalIndices(PathMode mode, int count)
{
var list = new List<int>();
if (count <= 0)
return list;
if (count == 1)
{
list.Add(0);
return list;
}
if (mode == PathMode.Loop)
{
for (var i = 0; i < count; i++)
list.Add(i);
return list;
}
for (var i = 0; i < count; i++)
list.Add(i);
for (var i = count - 2; i >= 1; i--)
list.Add(i);
return list;
}
void RotateTraversalToNearest(List<int> indices, Vector3 currentPosition)
{
if (indices == null || indices.Count == 0)
return;
var best = 0;
var bestDist = float.PositiveInfinity;
for (var i = 0; i < indices.Count; i++)
{
var wi = indices[i];
if (wi < 0 || wi >= waypoints.Count)
continue;
var p = waypoints[wi].point;
if (p == null)
continue;
var d = (p.position - currentPosition).sqrMagnitude;
if (d < bestDist)
{
bestDist = d;
best = i;
}
}
if (best <= 0)
return;
var rotated = new List<int>(indices.Count);
for (var i = best; i < indices.Count; i++)
rotated.Add(indices[i]);
for (var i = 0; i < best; i++)
rotated.Add(indices[i]);
indices.Clear();
indices.AddRange(rotated);
}
#if DOTWEEN
Tween CreateOneShotShakeTween(float duration)
{
if (shakeTransform == null)
return null;
var seq = DOTween.Sequence();
seq.SetUpdate(useUnscaledTime);
if (shakePosition && shakeStrengthPosition > 0f)
{
var t = shakeTransform.DOShakePosition(
duration,
shakeStrengthPosition,
shakeVibrato,
shakeRandomness,
false,
true
);
seq.Join(t);
}
if (shakeRotation && shakeStrengthRotation > 0f)
{
var t = shakeTransform.DOShakeRotation(
duration,
shakeStrengthRotation,
shakeVibrato,
shakeRandomness,
true
);
seq.Join(t);
}
return seq;
}
#endif
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 03105c644beb8724b86cea4ec224b18e
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,406 @@
using System;
using System.Collections.Generic;
using UnityEngine;
#if DOTWEEN
using DG.Tweening;
#endif
namespace Test_2022.CameraControl
{
[DisallowMultipleComponent]
public sealed class CameraWaypointSwitcherTweenedBackup : MonoBehaviour
{
public enum PathMode
{
Loop = 0,
PingPong = 1
}
public enum ShakeMode
{
Off = 0,
Continuous = 1,
WhileMoving = 2
}
[Serializable]
public struct Waypoint
{
public Transform point;
[Min(0f)] public float holdSeconds;
}
[Header("Target")]
[SerializeField] Transform target;
[SerializeField] bool autoStartOnEnable = true;
[SerializeField] bool useUnscaledTime;
[SerializeField] bool restoreOnDisable = true;
[Header("Path")]
[SerializeField] PathMode pathMode = PathMode.PingPong;
[SerializeField] List<Waypoint> waypoints = new List<Waypoint>();
[SerializeField, Min(0f)] float moveDurationSeconds = 1.2f;
[SerializeField, Min(0f)] float defaultHoldSeconds = 1.5f;
[SerializeField] AnimationCurve moveEase = AnimationCurve.EaseInOut(0f, 0f, 1f, 1f);
[SerializeField] bool startFromNearestWaypoint = true;
[Header("Shake")]
[SerializeField] ShakeMode shakeMode = ShakeMode.Continuous;
[SerializeField] bool createRigForShake = true;
[SerializeField] bool shakePosition = true;
[SerializeField] bool shakeRotation = true;
[SerializeField, Min(0f)] float shakeStrengthPosition = 0.06f;
[SerializeField, Min(0f)] float shakeStrengthRotation = 0.65f;
[SerializeField, Min(1)] int shakeVibrato = 12;
[SerializeField, Range(0f, 180f)] float shakeRandomness = 90f;
Vector3 basePosition;
Quaternion baseRotation;
Transform rig;
Transform shakeTransform;
#if DOTWEEN
Sequence sequence;
Tween shakePosTween;
Tween shakeRotTween;
#endif
public IList<Waypoint> Waypoints => waypoints;
void Reset()
{
var cam = Camera.main;
if (cam != null)
target = cam.transform;
}
void OnEnable()
{
if (autoStartOnEnable)
StartPlayback();
}
void OnDisable()
{
StopPlayback(restoreOnDisable);
}
public void StartPlayback()
{
#if DOTWEEN
if (target == null)
return;
if (waypoints == null || waypoints.Count == 0)
return;
StopPlayback(false);
CacheBasePose();
SetupRigAndShakeTransforms();
var moveTransform = rig != null ? rig : target;
var indices = BuildTraversalIndices(pathMode, waypoints.Count);
if (indices.Count == 0)
return;
if (startFromNearestWaypoint)
RotateTraversalToNearest(indices, moveTransform.position);
sequence = DOTween.Sequence();
sequence.SetUpdate(useUnscaledTime);
sequence.SetAutoKill(false);
for (var i = 0; i < indices.Count; i++)
{
var wp = waypoints[indices[i]].point;
if (wp == null)
continue;
var move = moveTransform.DOMove(wp.position, moveDurationSeconds);
move.SetEase(moveEase);
var rot = moveTransform.DORotateQuaternion(wp.rotation, moveDurationSeconds);
rot.SetEase(moveEase);
sequence.Append(move);
sequence.Join(rot);
if (shakeMode == ShakeMode.WhileMoving)
sequence.Join(CreateOneShotShakeTween(moveDurationSeconds));
sequence.AppendInterval(GetHoldSeconds(indices[i]));
}
sequence.SetLoops(-1, LoopType.Restart);
if (shakeMode == ShakeMode.Continuous)
StartContinuousShake();
sequence.Play();
#endif
}
public void StopPlayback(bool restore)
{
#if DOTWEEN
if (sequence != null)
{
sequence.Kill(false);
sequence = null;
}
if (shakePosTween != null)
{
shakePosTween.Kill(false);
shakePosTween = null;
}
if (shakeRotTween != null)
{
shakeRotTween.Kill(false);
shakeRotTween = null;
}
#endif
if (restore)
RestoreBasePose();
TeardownRigIfCreated();
}
public void ClearWaypoints()
{
waypoints.Clear();
}
public bool AddWaypoint(Transform point, float holdSeconds = -1f)
{
if (point == null)
return false;
for (var i = 0; i < waypoints.Count; i++)
{
if (waypoints[i].point == point)
return false;
}
waypoints.Add(new Waypoint
{
point = point,
holdSeconds = holdSeconds
});
return true;
}
public int RemoveNullWaypoints()
{
return waypoints.RemoveAll(w => w.point == null);
}
void CacheBasePose()
{
if (target == null)
return;
basePosition = target.position;
baseRotation = target.rotation;
}
void RestoreBasePose()
{
if (target == null)
return;
target.position = basePosition;
target.rotation = baseRotation;
}
void SetupRigAndShakeTransforms()
{
rig = null;
shakeTransform = target;
if (target == null)
return;
if (!createRigForShake || shakeMode == ShakeMode.Off)
return;
var rigGo = new GameObject(target.name + "_Rig");
rigGo.transform.SetPositionAndRotation(target.position, target.rotation);
rigGo.transform.SetParent(target.parent, true);
rig = rigGo.transform;
target.SetParent(rig, true);
shakeTransform = target;
target.localPosition = Vector3.zero;
target.localRotation = Quaternion.identity;
}
void TeardownRigIfCreated()
{
if (rig == null || target == null)
{
rig = null;
shakeTransform = null;
return;
}
if (target.parent == rig)
target.SetParent(rig.parent, true);
if (Application.isPlaying)
Destroy(rig.gameObject);
else
DestroyImmediate(rig.gameObject);
rig = null;
shakeTransform = null;
}
float GetHoldSeconds(int waypointIndex)
{
if (waypointIndex < 0 || waypointIndex >= waypoints.Count)
return defaultHoldSeconds;
var h = waypoints[waypointIndex].holdSeconds;
return h > 0f ? h : defaultHoldSeconds;
}
static List<int> BuildTraversalIndices(PathMode mode, int count)
{
var list = new List<int>();
if (count <= 0)
return list;
if (count == 1)
{
list.Add(0);
return list;
}
if (mode == PathMode.Loop)
{
for (var i = 0; i < count; i++)
list.Add(i);
return list;
}
for (var i = 0; i < count; i++)
list.Add(i);
for (var i = count - 2; i >= 1; i--)
list.Add(i);
return list;
}
void RotateTraversalToNearest(List<int> indices, Vector3 currentPosition)
{
if (indices == null || indices.Count == 0)
return;
var best = 0;
var bestDist = float.PositiveInfinity;
for (var i = 0; i < indices.Count; i++)
{
var wi = indices[i];
if (wi < 0 || wi >= waypoints.Count)
continue;
var p = waypoints[wi].point;
if (p == null)
continue;
var d = (p.position - currentPosition).sqrMagnitude;
if (d < bestDist)
{
bestDist = d;
best = i;
}
}
if (best <= 0)
return;
var rotated = new List<int>(indices.Count);
for (var i = best; i < indices.Count; i++)
rotated.Add(indices[i]);
for (var i = 0; i < best; i++)
rotated.Add(indices[i]);
indices.Clear();
indices.AddRange(rotated);
}
#if DOTWEEN
Tween CreateOneShotShakeTween(float duration)
{
if (shakeTransform == null)
return null;
var seq = DOTween.Sequence();
seq.SetUpdate(useUnscaledTime);
if (shakePosition && shakeStrengthPosition > 0f)
{
var t = shakeTransform.DOShakePosition(
duration,
shakeStrengthPosition,
shakeVibrato,
shakeRandomness,
false,
true
);
seq.Join(t);
}
if (shakeRotation && shakeStrengthRotation > 0f)
{
var t = shakeTransform.DOShakeRotation(
duration,
shakeStrengthRotation,
shakeVibrato,
shakeRandomness,
true
);
seq.Join(t);
}
return seq;
}
void StartContinuousShake()
{
if (shakeTransform == null)
return;
var duration = Mathf.Max(0.5f, moveDurationSeconds + defaultHoldSeconds);
if (shakePosition && shakeStrengthPosition > 0f)
{
shakePosTween = shakeTransform.DOShakePosition(
duration,
shakeStrengthPosition,
shakeVibrato,
shakeRandomness,
false,
true
)
.SetUpdate(useUnscaledTime)
.SetLoops(-1, LoopType.Restart);
}
if (shakeRotation && shakeStrengthRotation > 0f)
{
shakeRotTween = shakeTransform.DOShakeRotation(
duration,
shakeStrengthRotation,
shakeVibrato,
shakeRandomness,
true
)
.SetUpdate(useUnscaledTime)
.SetLoops(-1, LoopType.Restart);
}
}
#endif
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: c6d753c21b4298e47980bc9cbb200e38
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 0284fbf29b19c2a44865a3082d62d427
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,209 @@
using System.Collections.Generic;
using UnityEditor;
using UnityEditor.SceneManagement;
using UnityEngine;
using Test_2022.CameraControl;
[CustomEditor(typeof(CameraWaypointSwitcher))]
[CanEditMultipleObjects]
public sealed class CameraWaypointSwitcherEditor : Editor
{
SerializedProperty targetProp;
SerializedProperty waypointsProp;
void OnEnable()
{
targetProp = serializedObject.FindProperty("target");
waypointsProp = serializedObject.FindProperty("waypoints");
}
public override void OnInspectorGUI()
{
serializedObject.Update();
DrawDefaultInspector();
serializedObject.ApplyModifiedProperties();
EditorGUILayout.Space(10);
using (new EditorGUILayout.VerticalScope(EditorStyles.helpBox))
{
EditorGUILayout.LabelField("Tools", EditorStyles.boldLabel);
using (new EditorGUILayout.HorizontalScope())
{
if (GUILayout.Button("Use Main Camera"))
UseMainCamera();
if (GUILayout.Button("Create Point From Target"))
CreatePointFromTarget();
}
using (new EditorGUILayout.HorizontalScope())
{
if (GUILayout.Button("Add Selected"))
AddSelected(false);
if (GUILayout.Button("Add Selected (+Children)"))
AddSelected(true);
}
using (new EditorGUILayout.HorizontalScope())
{
if (GUILayout.Button("Remove Nulls"))
RemoveNulls();
if (GUILayout.Button("Clear Points"))
ClearPoints();
}
}
EditorGUILayout.Space(10);
using (new EditorGUILayout.VerticalScope(EditorStyles.helpBox))
{
EditorGUILayout.LabelField("Runtime", EditorStyles.boldLabel);
if (!Application.isPlaying)
EditorGUILayout.HelpBox("进入 Play 模式后才会执行点位切换与摇晃。Transition Mode = Instant 为瞬切,Tween 为平滑移动。Shake Mode = Handheld 为手持式平滑摇晃。", MessageType.Info);
using (new EditorGUI.DisabledScope(!Application.isPlaying))
{
using (new EditorGUILayout.HorizontalScope())
{
if (GUILayout.Button("Start"))
StartPlayback();
if (GUILayout.Button("Stop (Restore)"))
StopPlayback(true);
if (GUILayout.Button("Stop (No Restore)"))
StopPlayback(false);
}
}
}
}
void UseMainCamera()
{
var cam = Camera.main;
if (cam == null)
return;
foreach (var t in targets)
{
Undo.RecordObject(t, "Assign Main Camera");
var sw = (CameraWaypointSwitcher)t;
var so = new SerializedObject(sw);
so.FindProperty("target").objectReferenceValue = cam.transform;
so.ApplyModifiedPropertiesWithoutUndo();
EditorUtility.SetDirty(sw);
MarkSceneDirty(sw);
}
serializedObject.Update();
}
void CreatePointFromTarget()
{
if (targetProp == null)
return;
var target = targetProp.objectReferenceValue as Transform;
if (target == null)
return;
var go = new GameObject("CamPoint");
Undo.RegisterCreatedObjectUndo(go, "Create Camera Point");
go.transform.SetPositionAndRotation(target.position, target.rotation);
AddPointToAll(go.transform, -1f);
Selection.activeGameObject = go;
}
void AddSelected(bool includeChildren)
{
var selected = Selection.gameObjects;
if (selected == null || selected.Length == 0)
return;
var points = new List<Transform>();
for (var i = 0; i < selected.Length; i++)
{
var go = selected[i];
if (go == null)
continue;
if (includeChildren)
points.AddRange(go.GetComponentsInChildren<Transform>(true));
else
points.Add(go.transform);
}
for (var i = 0; i < points.Count; i++)
AddPointToAll(points[i], -1f);
}
void AddPointToAll(Transform point, float holdSeconds)
{
if (point == null)
return;
foreach (var t in targets)
{
var sw = (CameraWaypointSwitcher)t;
Undo.RecordObject(sw, "Add Waypoint");
sw.AddWaypoint(point, holdSeconds);
EditorUtility.SetDirty(sw);
MarkSceneDirty(sw);
}
}
void RemoveNulls()
{
foreach (var t in targets)
{
var sw = (CameraWaypointSwitcher)t;
Undo.RecordObject(sw, "Remove Null Waypoints");
sw.RemoveNullWaypoints();
EditorUtility.SetDirty(sw);
MarkSceneDirty(sw);
}
}
void ClearPoints()
{
foreach (var t in targets)
{
var sw = (CameraWaypointSwitcher)t;
Undo.RecordObject(sw, "Clear Waypoints");
sw.ClearWaypoints();
EditorUtility.SetDirty(sw);
MarkSceneDirty(sw);
}
}
void StartPlayback()
{
foreach (var t in targets)
{
var sw = (CameraWaypointSwitcher)t;
sw.StartPlayback();
}
}
void StopPlayback(bool restore)
{
foreach (var t in targets)
{
var sw = (CameraWaypointSwitcher)t;
sw.StopPlayback(restore);
}
}
static void MarkSceneDirty(CameraWaypointSwitcher sw)
{
if (Application.isPlaying)
return;
if (!sw.gameObject.scene.IsValid())
return;
EditorSceneManager.MarkSceneDirty(sw.gameObject.scene);
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 523a055464a634349bc2aad4daf8faf1
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+8
View File
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 63db049d8ba02104a9919d76e4ae7a48
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 7ada7dd9299b1af419e0def96a775885
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,42 @@
using UnityEngine;
namespace Core.Actions
{
public class DestroyGameObjectAction : GameAction
{
[SerializeField] private GameObject target;
[SerializeField] private GameObject[] additionalTargets;
[SerializeField] private float delaySeconds = 0f;
[SerializeField] private bool destroyThisGameObjectIfTargetMissing = true;
public override void Invoke()
{
bool destroyedAny = false;
if (target != null)
{
destroyedAny = true;
if (delaySeconds > 0f) Destroy(target, delaySeconds);
else Destroy(target);
}
if (additionalTargets != null)
{
for (int i = 0; i < additionalTargets.Length; i++)
{
GameObject go = additionalTargets[i];
if (go == null) continue;
destroyedAny = true;
if (delaySeconds > 0f) Destroy(go, delaySeconds);
else Destroy(go);
}
}
if (!destroyedAny && destroyThisGameObjectIfTargetMissing)
{
if (delaySeconds > 0f) Destroy(gameObject, delaySeconds);
else Destroy(gameObject);
}
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: ac986c3420f8b524c8756b0d840d1156
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,9 @@
using UnityEngine;
namespace Core.Actions
{
public abstract class GameAction : MonoBehaviour
{
public abstract void Invoke();
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: d9dc21bb788e5bf4da1fef954fdf097e
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 5b8a3eab83b83354c945606694075b86
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,176 @@
using System.Collections.Generic;
using UnityEngine;
#if ENABLE_INPUT_SYSTEM
using UnityEngine.InputSystem;
#endif
namespace Core.InputLock
{
public class PlayerControlLockService : MonoBehaviour
{
[SerializeField] private bool debugLogs = true;
public static PlayerControlLockService Instance { get; private set; }
private readonly HashSet<object> owners = new HashSet<object>();
private readonly Dictionary<Behaviour, bool> cachedEnabled = new Dictionary<Behaviour, bool>();
private CursorLockMode previousCursorLockMode;
private bool previousCursorVisible;
private bool hasCachedCursorState;
private Player.PlayerController cachedPlayerController;
#if ENABLE_INPUT_SYSTEM
private PlayerInput cachedPlayerInput;
private string previousActionMap;
#endif
private void Awake()
{
if (Instance == null)
{
Instance = this;
DontDestroyOnLoad(gameObject);
}
else
{
Destroy(gameObject);
}
}
private void Update()
{
int before = owners.Count;
PruneOwners();
if (before > 0 && owners.Count == 0)
{
ApplyLockedState(false);
}
}
public void Lock(object owner)
{
if (owner == null) { return; }
PruneOwners();
bool wasEmpty = owners.Count == 0;
owners.Add(owner);
if (wasEmpty && owners.Count == 1)
{
if (debugLogs) Debug.Log($"[PlayerControlLock] Lock by {owner}. Switching to locked state.");
ApplyLockedState(true);
}
}
public void Unlock(object owner)
{
if (owner == null) { return; }
PruneOwners();
owners.Remove(owner);
if (owners.Count == 0)
{
if (debugLogs) Debug.Log($"[PlayerControlLock] Unlock by {owner}. Restoring state.");
ApplyLockedState(false);
}
}
public bool IsLocked() => owners.Count > 0;
private void ApplyLockedState(bool locked)
{
if (locked)
{
cachedEnabled.Clear();
cachedPlayerController = null;
if (!hasCachedCursorState)
{
previousCursorLockMode = Cursor.lockState;
previousCursorVisible = Cursor.visible;
hasCachedCursorState = true;
}
cachedPlayerController = FindObjectOfType<Player.PlayerController>(true);
if (cachedPlayerController != null) cachedPlayerController.ResetInputState();
CacheAndSetEnabled(cachedPlayerController, false);
CacheAndSetEnabled(FindObjectOfType<ItemUsageSystem>(true), false);
CacheAndSetEnabled(FindObjectOfType<InventoryUI>(true), false);
Cursor.lockState = CursorLockMode.None;
Cursor.visible = true;
#if ENABLE_INPUT_SYSTEM
cachedPlayerInput = FindPrimaryPlayerInput();
if (cachedPlayerInput != null)
{
previousActionMap = cachedPlayerInput.currentActionMap != null ? cachedPlayerInput.currentActionMap.name : null;
cachedPlayerInput.SwitchCurrentActionMap("UI");
if (debugLogs) Debug.Log($"[PlayerControlLock] Switched action map {previousActionMap} -> UI");
}
#endif
}
else
{
foreach (var kv in cachedEnabled)
{
if (kv.Key != null)
{
kv.Key.enabled = kv.Value;
}
}
cachedEnabled.Clear();
if (cachedPlayerController != null) cachedPlayerController.ResetInputState();
cachedPlayerController = null;
if (hasCachedCursorState)
{
Cursor.lockState = previousCursorLockMode;
Cursor.visible = previousCursorVisible;
}
else
{
Cursor.lockState = CursorLockMode.Locked;
Cursor.visible = false;
}
hasCachedCursorState = false;
#if ENABLE_INPUT_SYSTEM
if (cachedPlayerInput != null)
{
cachedPlayerInput.SwitchCurrentActionMap(string.IsNullOrWhiteSpace(previousActionMap) ? "Player" : previousActionMap);
if (debugLogs) Debug.Log($"[PlayerControlLock] Restored action map -> {(string.IsNullOrWhiteSpace(previousActionMap) ? "Player" : previousActionMap)}");
}
cachedPlayerInput = null;
previousActionMap = null;
#endif
}
}
#if ENABLE_INPUT_SYSTEM
private static PlayerInput FindPrimaryPlayerInput()
{
Player.PlayerController playerController = FindObjectOfType<Player.PlayerController>(true);
if (playerController != null)
{
PlayerInput playerInput = playerController.GetComponentInChildren<PlayerInput>(true);
if (playerInput != null) { return playerInput; }
}
return FindObjectOfType<PlayerInput>(true);
}
#endif
private void CacheAndSetEnabled(Behaviour behaviour, bool enabled)
{
if (behaviour == null) { return; }
cachedEnabled[behaviour] = behaviour.enabled;
behaviour.enabled = enabled;
}
private void PruneOwners()
{
owners.RemoveWhere(o =>
{
if (o == null) { return true; }
if (o is UnityEngine.Object uo) { return uo == null; }
return false;
});
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: f740f4129d802dc44a26d2131c909048
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 8494bca840439914abb8abba140567d0
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,9 @@
using UnityEngine;
public interface IInteractable
{
public void Interact()
{
Debug.Log("Interact");
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: bfc87b6ad6117694da03640223a57c42
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,24 @@
using System;
using System.Collections;
namespace LLM
{
// 通用 LLM 服务接口
public interface ILLMService
{
// 发送消息,callback 返回 (回复内容, 是否成功)
IEnumerator SendMessage(string userMessage, Action<string, bool> callback);
// 清除历史记录(如果需要)
void ClearHistory();
// 设置/恢复历史记录
void SetHistory(System.Collections.Generic.List<Message> history);
// 更新 System Prompt (用于动态注入记忆)
void UpdateSystemPrompt(string newPrompt);
// 发送不带历史记录的一次性消息 (用于总结等后台任务)
IEnumerator SendStatelessMessage(System.Collections.Generic.List<Message> messages, Action<string, bool> callback);
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 50a6434288b88ad499daa079d64095ec
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,9 @@
fileFormatVersion: 2
guid: 2b3c4d5e6f7081920a1b2c3d4e5f6071
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,25 @@
using System.Collections.Generic;
using UnityEngine;
namespace Core.NarrationSystem
{
[System.Serializable]
public class NarrationPageData
{
[UnityEngine.TextArea(3, 12)] public string text;
[Tooltip("The Action ID to trigger when this page is shown")]
public string enterActionId;
[Tooltip("The Action ID to trigger when continuing from this page")]
public string continueActionId;
}
[CreateAssetMenu(fileName = "NewNarrationData", menuName = "Systems/Narration/Narration Data")]
public class NarrationDataSO : ScriptableObject
{
[Tooltip("Unique identifier for this narration sequence")]
public string id;
[Tooltip("List of narration pages")]
public List<NarrationPageData> pages = new List<NarrationPageData>();
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 943e86b80c439184ba6d48061fbffee9
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,60 @@
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;
namespace Core.NarrationSystem
{
[System.Serializable]
public class NarrationActionMapping
{
public string actionId;
public UnityEvent onTrigger;
}
public class NarrationEventListener : MonoBehaviour
{
[SerializeField] private List<NarrationActionMapping> actionMappings = new List<NarrationActionMapping>();
private void OnEnable()
{
if (NarrationSystem.Instance != null)
{
NarrationSystem.Instance.OnNarrationActionTriggered += HandleNarrationAction;
}
else
{
Invoke(nameof(SubscribeWithDelay), 0.1f);
}
}
private void SubscribeWithDelay()
{
if (NarrationSystem.Instance != null)
{
NarrationSystem.Instance.OnNarrationActionTriggered -= HandleNarrationAction;
NarrationSystem.Instance.OnNarrationActionTriggered += HandleNarrationAction;
}
}
private void OnDisable()
{
if (NarrationSystem.Instance != null)
{
NarrationSystem.Instance.OnNarrationActionTriggered -= HandleNarrationAction;
}
}
private void HandleNarrationAction(string actionId)
{
if (string.IsNullOrWhiteSpace(actionId)) return;
foreach (var mapping in actionMappings)
{
if (mapping.actionId == actionId)
{
mapping.onTrigger?.Invoke();
}
}
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 1fb89910bcb527a4cb612dae6f8afc1b
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,25 @@
using UnityEngine;
using Core.Actions;
namespace Core.NarrationSystem
{
public class NarrationPlayByIdAction : GameAction
{
[SerializeField] private NarrationSystem narrationSystem;
[SerializeField] private string sequenceId;
[SerializeField] private bool debugLog;
public override void Invoke()
{
NarrationSystem system = narrationSystem != null ? narrationSystem : NarrationSystem.Instance;
if (system == null)
{
if (debugLog) Debug.Log("[NarrationPlayByIdAction] NarrationSystem is null", this);
return;
}
bool ok = system.PlayById(sequenceId);
if (debugLog && !ok) Debug.LogWarning($"[NarrationPlayByIdAction] PlayById failed id='{sequenceId}'", this);
}
}
}
@@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: 7c6d5e4f3a2b1c0d9e8f7a6b5c4d3e2f
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,181 @@
using System;
using System.Collections.Generic;
using Core.InputLock;
using UI.Narration;
using UnityEngine;
namespace Core.NarrationSystem
{
public class NarrationSystem : MonoBehaviour
{
public static NarrationSystem Instance { get; private set; }
[Header("Lifetime")]
[SerializeField] private bool dontDestroyOnLoad = true;
[Header("UI")]
[SerializeField] private NarrationPanelView panelView;
[Header("Data")]
[SerializeField] private List<NarrationDataSO> narrationDatabase = new List<NarrationDataSO>();
[Header("Input Lock")]
[SerializeField] private bool lockPlayerControlsWhileShowing = true;
public event Action Finished;
public event Action<string> OnNarrationActionTriggered;
readonly Dictionary<string, NarrationDataSO> sequencesById = new Dictionary<string, NarrationDataSO>();
NarrationDataSO currentSequence;
int currentIndex;
public bool IsPlaying => currentSequence != null;
void Awake()
{
if (Instance != null && Instance != this)
{
Destroy(gameObject);
return;
}
Instance = this;
if (dontDestroyOnLoad) { DontDestroyOnLoad(gameObject); }
}
void Start()
{
RebuildSequenceCache();
if (panelView != null)
{
panelView.onContinue.AddListener(Continue);
panelView.Hide();
}
}
void OnDestroy()
{
if (panelView != null)
{
panelView.onContinue.RemoveListener(Continue);
}
if (Instance == this) { Instance = null; }
}
public void RebuildSequenceCache()
{
sequencesById.Clear();
foreach (var seq in narrationDatabase)
{
if (seq == null) { continue; }
if (string.IsNullOrWhiteSpace(seq.id)) { continue; }
sequencesById[seq.id] = seq;
}
}
public bool PlayById(string sequenceId)
{
if (string.IsNullOrWhiteSpace(sequenceId)) { return false; }
if (sequencesById.Count == 0) { RebuildSequenceCache(); }
if (!sequencesById.TryGetValue(sequenceId, out NarrationDataSO sequence) || sequence == null) { return false; }
Play(sequence);
return true;
}
public void Play(NarrationDataSO sequence)
{
if (sequence == null) { return; }
currentSequence = sequence;
currentIndex = 0;
if (lockPlayerControlsWhileShowing && PlayerControlLockService.Instance != null)
{
PlayerControlLockService.Instance.Lock(this);
}
if (panelView != null) { panelView.Show(); }
ShowCurrentPage(invokeEnterEvent: true);
}
public void Stop()
{
if (currentSequence == null) { return; }
currentSequence = null;
currentIndex = 0;
if (panelView != null) { panelView.Hide(); }
if (lockPlayerControlsWhileShowing && PlayerControlLockService.Instance != null)
{
PlayerControlLockService.Instance.Unlock(this);
}
Finished?.Invoke();
}
public void Continue()
{
if (currentSequence == null) { return; }
IReadOnlyList<NarrationPageData> pages = currentSequence.pages;
if (pages == null || pages.Count == 0)
{
Stop();
return;
}
if (currentIndex >= 0 && currentIndex < pages.Count)
{
var page = pages[currentIndex];
if (!string.IsNullOrWhiteSpace(page.continueActionId))
{
OnNarrationActionTriggered?.Invoke(page.continueActionId);
}
}
currentIndex++;
if (currentIndex >= pages.Count)
{
Stop();
return;
}
ShowCurrentPage(invokeEnterEvent: true);
}
void ShowCurrentPage(bool invokeEnterEvent)
{
if (currentSequence == null) { return; }
IReadOnlyList<NarrationPageData> pages = currentSequence.pages;
if (pages == null || pages.Count == 0) { return; }
currentIndex = Mathf.Clamp(currentIndex, 0, pages.Count - 1);
NarrationPageData page = pages[currentIndex];
if (page == null) { return; }
if (panelView != null)
{
panelView.SetText(page.text);
panelView.SetIsLastPage(currentIndex == pages.Count - 1);
}
if (invokeEnterEvent)
{
if (!string.IsNullOrWhiteSpace(page.enterActionId))
{
OnNarrationActionTriggered?.Invoke(page.enterActionId);
}
}
}
}
}
@@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: 8f7e6d5c4b3a29180796a5b4c3d2e1f0
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 91bf37b87aba3484a82cce7fe5a2cd48
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,14 @@
namespace Core.SaveSystem
{
public interface ISaveable
{
// 返回唯一标识符 (例如 "ChatSystem", "Inventory")
string GetSaveID();
// 捕获当前状态,返回可序列化的 JSON 字符串
string CaptureState();
// 从 JSON 字符串恢复状态
void RestoreState(string stateJson);
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: fdc7732ae1c146d4b99d2343f20d9740
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,113 @@
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
namespace Core.SaveSystem
{
public class PersistentFlagsService : MonoBehaviour, ISaveable
{
[SerializeField] private bool debugLogs = true;
[SerializeField] private string saveId = "PersistentFlags";
public static PersistentFlagsService Instance { get; private set; }
private readonly HashSet<string> flags = new HashSet<string>();
[Serializable]
private class State
{
public List<string> flags = new List<string>();
}
private void Awake()
{
if (Instance == null)
{
Instance = this;
DontDestroyOnLoad(gameObject);
}
else
{
Destroy(gameObject);
return;
}
}
private void OnEnable()
{
if (SaveManager.Instance != null)
{
SaveManager.Instance.Register(this);
}
}
private void OnDisable()
{
if (SaveManager.Instance != null)
{
SaveManager.Instance.Unregister(this);
}
}
public bool IsSet(string flagId)
{
if (string.IsNullOrWhiteSpace(flagId)) { return false; }
return flags.Contains(flagId);
}
public bool TrySet(string flagId)
{
if (string.IsNullOrWhiteSpace(flagId)) { return false; }
bool added = flags.Add(flagId);
if (added && debugLogs) Debug.Log($"[PersistentFlags] Set {flagId}");
return added;
}
public void Clear(string flagId)
{
if (string.IsNullOrWhiteSpace(flagId)) { return; }
if (flags.Remove(flagId) && debugLogs) Debug.Log($"[PersistentFlags] Cleared {flagId}");
}
public void SaveNow()
{
if (SaveManager.Instance != null)
{
SaveManager.Instance.SaveGame();
}
}
public string GetSaveID() => saveId;
public string CaptureState()
{
var state = new State
{
flags = flags.OrderBy(s => s).ToList()
};
return JsonUtility.ToJson(state);
}
public void RestoreState(string jsonState)
{
flags.Clear();
if (string.IsNullOrWhiteSpace(jsonState)) { return; }
try
{
var state = JsonUtility.FromJson<State>(jsonState);
if (state?.flags == null) { return; }
for (int i = 0; i < state.flags.Count; i++)
{
string key = state.flags[i];
if (!string.IsNullOrWhiteSpace(key)) { flags.Add(key); }
}
}
catch
{
}
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 844967dfb11645242b3abc694baba0d9
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,122 @@
using UnityEngine;
using System.Collections.Generic;
using System.IO;
using System.Linq;
namespace Core.SaveSystem
{
public class SaveManager : MonoBehaviour
{
public static SaveManager Instance { get; private set; }
private const string SAVE_FILE_NAME = "savegame.json";
// 注册的所有需要存档的系统
private List<ISaveable> saveables = new List<ISaveable>();
// 缓存已加载的数据,以便稍后注册的系统也能获取
private Dictionary<string, string> loadedDataCache = new Dictionary<string, string>();
[System.Serializable]
private class SaveDataWrapper
{
public List<string> keys = new List<string>();
public List<string> values = new List<string>();
}
private void Awake()
{
if (Instance == null)
{
Instance = this;
DontDestroyOnLoad(gameObject);
}
else
{
Destroy(gameObject);
}
}
public void Register(ISaveable saveable)
{
if (!saveables.Contains(saveable))
{
saveables.Add(saveable);
// 如果缓存中有该系统的数据,立即恢复
string id = saveable.GetSaveID();
if (loadedDataCache.TryGetValue(id, out string jsonState))
{
saveable.RestoreState(jsonState);
Debug.Log($"[SaveManager] 系统 {id} 已注册并恢复状态。");
}
}
}
public void Unregister(ISaveable saveable)
{
saveables.Remove(saveable);
}
public void SaveGame()
{
SaveDataWrapper wrapper = new SaveDataWrapper();
foreach (var saveable in saveables)
{
string id = saveable.GetSaveID();
string json = saveable.CaptureState();
wrapper.keys.Add(id);
wrapper.values.Add(json);
}
string finalJson = JsonUtility.ToJson(wrapper, true);
string path = Path.Combine(Application.persistentDataPath, SAVE_FILE_NAME);
File.WriteAllText(path, finalJson);
Debug.Log($"[SaveManager] 游戏已保存至: {path}");
}
public void LoadGame()
{
string path = Path.Combine(Application.persistentDataPath, SAVE_FILE_NAME);
if (!File.Exists(path))
{
Debug.LogWarning("[SaveManager] 未找到存档文件。");
return;
}
try
{
string finalJson = File.ReadAllText(path);
SaveDataWrapper wrapper = JsonUtility.FromJson<SaveDataWrapper>(finalJson);
if (wrapper == null || wrapper.keys == null) return;
// 构建字典以便快速查找
loadedDataCache.Clear();
for (int i = 0; i < wrapper.keys.Count; i++)
{
if (i < wrapper.values.Count)
{
loadedDataCache[wrapper.keys[i]] = wrapper.values[i];
}
}
// 分发数据给已注册的系统
foreach (var saveable in saveables)
{
string id = saveable.GetSaveID();
if (loadedDataCache.TryGetValue(id, out string jsonState))
{
saveable.RestoreState(jsonState);
}
}
Debug.Log("[SaveManager] 游戏已加载。");
}
catch (System.Exception e)
{
Debug.LogError($"[SaveManager] 加载失败: {e.Message}");
}
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 433f5518fc9b27d4cb1affbcdaac4998
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: b951d361185f9244a8a165f716d7fd18
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,111 @@
using System.Collections;
using Michsky.UI.Reach;
using UnityEngine;
using UnityEngine.SceneManagement;
namespace Core.SceneLoading
{
public class StartupSceneLoader : MonoBehaviour
{
public static StartupSceneLoader Instance { get; private set; }
[Header("Scene")]
[SerializeField] int nextSceneBuildIndex = 1;
[Header("UI")]
[SerializeField] ProgressBar progressBar;
[SerializeField] float smoothSpeed = 4f;
static bool isLoadComplete;
static float progress01;
AsyncOperation loadOperation;
float displayedProgress01;
void Awake()
{
if (Instance != null && Instance != this)
{
Destroy(gameObject);
return;
}
Instance = this;
DontDestroyOnLoad(gameObject);
}
void Start()
{
BeginLoad();
}
public void BeginLoad()
{
if (loadOperation != null) return;
if (nextSceneBuildIndex < 0 || nextSceneBuildIndex >= SceneManager.sceneCountInBuildSettings)
{
Debug.LogError($"[StartupSceneLoader] 无效的场景 BuildIndex: {nextSceneBuildIndex}");
return;
}
if (progressBar == null)
{
progressBar = FindObjectOfType<ProgressBar>();
}
isLoadComplete = false;
progress01 = 0f;
displayedProgress01 = 0f;
ApplyProgressToBar(0f);
StartCoroutine(LoadRoutine());
}
public bool IsLoadComplete() => isLoadComplete;
public static bool IsLoadCompleteStatic() => isLoadComplete;
public static float GetProgress01() => progress01;
public void ActivateLoadedScene()
{
if (loadOperation == null) return;
loadOperation.allowSceneActivation = true;
}
IEnumerator LoadRoutine()
{
loadOperation = SceneManager.LoadSceneAsync(nextSceneBuildIndex);
if (loadOperation == null) yield break;
loadOperation.allowSceneActivation = false;
while (loadOperation.progress < 0.9f)
{
float targetProgress01 = Mathf.Clamp01(loadOperation.progress / 0.9f);
progress01 = targetProgress01;
displayedProgress01 = Mathf.MoveTowards(displayedProgress01, targetProgress01, smoothSpeed * Time.unscaledDeltaTime);
ApplyProgressToBar(displayedProgress01);
yield return null;
}
progress01 = 1f;
displayedProgress01 = 1f;
ApplyProgressToBar(1f);
isLoadComplete = true;
while (!loadOperation.isDone)
{
yield return null;
}
}
void ApplyProgressToBar(float progress01Value)
{
if (progressBar == null) return;
progressBar.minValue = 0f;
progressBar.maxValue = 100f;
progressBar.SetValue(progress01Value * 100f);
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 93515f745a5525f41aa91c91fe23c590
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 0e4fe67f7c0128545b97a2648bd872e8
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,79 @@
using UnityEngine;
using UnityEngine.Audio;
using UI.PanelStack;
using Core.InputLock;
using Core.SaveSystem;
namespace Core.SettingsSystem
{
public class SettingsBootstrap : MonoBehaviour
{
[SerializeField] private AudioMixer audioMixer;
[SerializeField] private string masterVolumeParam = "MasterVolume";
[SerializeField] private string musicVolumeParam = "MusicVolume";
[SerializeField] private string sfxVolumeParam = "SFXVolume";
[SerializeField] private bool createLegacyEscapeListener = false;
[SerializeField] private bool debugLogs = true;
private void Awake()
{
if (debugLogs) Debug.Log($"[SettingsBootstrap] Awake. legacyEscapeListener={createLegacyEscapeListener}");
EnsurePanelStack();
EnsurePlayerControlLock();
EnsureSaveManager();
EnsurePersistentFlags();
if (SettingsService.Instance == null)
{
var go = new GameObject("SettingsService");
var service = go.AddComponent<SettingsService>();
if (audioMixer != null)
{
service.ConfigureAudioMixer(audioMixer, masterVolumeParam, musicVolumeParam, sfxVolumeParam);
}
}
else if (audioMixer != null)
{
SettingsService.Instance.ConfigureAudioMixer(audioMixer, masterVolumeParam, musicVolumeParam, sfxVolumeParam);
}
}
private void EnsurePanelStack()
{
if (UIPanelStack.Instance != null) { return; }
var go = new GameObject("UIPanelStack");
go.AddComponent<UIPanelStack>();
if (createLegacyEscapeListener)
{
go.AddComponent<UIPanelStackInput>();
}
go.AddComponent<UIActionMapRouter>();
go.AddComponent<PlayerInputActionRouter>();
if (debugLogs) Debug.Log("[SettingsBootstrap] Created UIPanelStack (+Router).");
}
private void EnsurePlayerControlLock()
{
if (PlayerControlLockService.Instance != null) { return; }
var go = new GameObject("PlayerControlLockService");
go.AddComponent<PlayerControlLockService>();
if (debugLogs) Debug.Log("[SettingsBootstrap] Created PlayerControlLockService.");
}
private void EnsureSaveManager()
{
if (SaveManager.Instance != null) { return; }
var go = new GameObject("SaveManager");
go.AddComponent<SaveManager>();
if (debugLogs) Debug.Log("[SettingsBootstrap] Created SaveManager.");
}
private void EnsurePersistentFlags()
{
if (PersistentFlagsService.Instance != null) { return; }
var go = new GameObject("PersistentFlagsService");
go.AddComponent<PersistentFlagsService>();
if (debugLogs) Debug.Log("[SettingsBootstrap] Created PersistentFlagsService.");
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: da4e9acb0432a2e4fbeeca068acd0fb3
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,95 @@
using System;
using UnityEngine;
namespace Core.SettingsSystem
{
[Serializable]
public class SettingsData
{
public int version = 1;
public GraphicsSettings graphics = new GraphicsSettings();
public AudioSettings audio = new AudioSettings();
public GameplaySettings gameplay = new GameplaySettings();
public static SettingsData Default()
{
return new SettingsData
{
version = 1,
graphics = GraphicsSettings.Default(),
audio = AudioSettings.Default(),
gameplay = GameplaySettings.Default()
};
}
[Serializable]
public class GraphicsSettings
{
public int fullscreenMode = (int)FullScreenMode.FullScreenWindow;
public int resolutionIndex = -1;
public int resolutionWidth = 0;
public int resolutionHeight = 0;
public int resolutionRefreshRate = 0;
public int qualityLevel = -1;
public bool vSync = false;
public int targetFps = 60;
public float renderScale = 1f;
public int msaaSampleCount = 1;
public float shadowDistance = 50f;
public int textureMipmapLimit = 0;
public static GraphicsSettings Default()
{
return new GraphicsSettings
{
fullscreenMode = (int)FullScreenMode.FullScreenWindow,
resolutionIndex = -1,
resolutionWidth = 0,
resolutionHeight = 0,
resolutionRefreshRate = 0,
qualityLevel = -1,
vSync = false,
targetFps = 60,
renderScale = 1f,
msaaSampleCount = 1,
shadowDistance = 50f,
textureMipmapLimit = 0
};
}
}
[Serializable]
public class AudioSettings
{
public float master = 1f;
public float music = 1f;
public float sfx = 1f;
public static AudioSettings Default()
{
return new AudioSettings
{
master = 1f,
music = 1f,
sfx = 1f
};
}
}
[Serializable]
public class GameplaySettings
{
public float mouseSensitivity = 1f;
public int llmProvider = 0;
public static GameplaySettings Default()
{
return new GameplaySettings
{
mouseSensitivity = 1f,
llmProvider = 0
};
}
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: b1b0531ff911c3744a9f306d2622fd76
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,359 @@
using System;
using System.IO;
using System.Reflection;
using UnityEngine;
using UnityEngine.Audio;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;
using UnityEngine.SceneManagement;
using Player;
namespace Core.SettingsSystem
{
public class SettingsService : MonoBehaviour
{
public static SettingsService Instance { get; private set; }
[Header("Debug")]
[SerializeField] private bool debugLogs = true;
[Header("Persistence")]
[SerializeField] private string fileName = "settings.json";
[Header("Audio (Optional)")]
[SerializeField] private AudioMixer audioMixer;
[SerializeField] private string masterVolumeParam = "MasterVolume";
[SerializeField] private string musicVolumeParam = "MusicVolume";
[SerializeField] private string sfxVolumeParam = "SFXVolume";
public SettingsData Current { get; private set; } = SettingsData.Default();
public string FilePath => Path.Combine(Application.persistentDataPath, fileName);
public void ConfigureAudioMixer(AudioMixer mixer, string masterParam, string musicParam, string sfxParam)
{
audioMixer = mixer;
if (!string.IsNullOrWhiteSpace(masterParam)) { masterVolumeParam = masterParam; }
if (!string.IsNullOrWhiteSpace(musicParam)) { musicVolumeParam = musicParam; }
if (!string.IsNullOrWhiteSpace(sfxParam)) { sfxVolumeParam = sfxParam; }
ApplyAudio(Current.audio);
}
private void Awake()
{
if (Instance == null)
{
Instance = this;
DontDestroyOnLoad(gameObject);
LoadOrCreate();
if (debugLogs) Debug.Log($"[SettingsService] Awake. loaded=true file={FilePath}");
ApplyToRuntime(Current);
}
else
{
Destroy(gameObject);
}
}
private void OnEnable()
{
SceneManager.sceneLoaded += OnSceneLoaded;
}
private void OnDisable()
{
SceneManager.sceneLoaded -= OnSceneLoaded;
}
public void LoadOrCreate()
{
if (!File.Exists(FilePath))
{
Current = SettingsData.Default();
SaveToDisk(Current);
if (debugLogs) Debug.Log($"[SettingsService] Created defaults file={FilePath}");
return;
}
try
{
string json = File.ReadAllText(FilePath);
SettingsData loaded = JsonUtility.FromJson<SettingsData>(json);
Current = loaded ?? SettingsData.Default();
}
catch (Exception e)
{
Current = SettingsData.Default();
if (debugLogs) Debug.LogError($"[SettingsService] Failed to read settings. file={FilePath} err={e.GetType().Name}:{e.Message}");
}
MigrateLegacyResolution(Current);
if (Sanitize(Current))
{
SaveToDisk(Current);
}
}
public void ApplyAndSave(SettingsData data)
{
if (data == null) { return; }
Current = data;
if (debugLogs) Debug.Log($"[SettingsService] ApplyAndSave. gameplay.mouseSensitivity={Current.gameplay?.mouseSensitivity}");
ApplyToRuntime(Current);
SaveToDisk(Current);
}
public void ResetToDefaults(bool applyAndSave)
{
Current = SettingsData.Default();
if (debugLogs) Debug.Log($"[SettingsService] ResetToDefaults. applyAndSave={applyAndSave} gameplay.mouseSensitivity={Current.gameplay?.mouseSensitivity}");
ApplyToRuntime(Current);
if (applyAndSave) { SaveToDisk(Current); }
}
public void ApplyToRuntime(SettingsData data)
{
if (data == null) { return; }
ApplyGraphics(data.graphics);
ApplyAudio(data.audio);
ApplyGameplay(data.gameplay);
}
private void OnSceneLoaded(Scene scene, LoadSceneMode mode)
{
if (debugLogs) Debug.Log($"[SettingsService] SceneLoaded. name={scene.name} mode={mode} gameplay.mouseSensitivity={Current.gameplay?.mouseSensitivity}");
ApplyGameplay(Current.gameplay);
}
private void ApplyGameplay(SettingsData.GameplaySettings g)
{
if (g == null) { return; }
PlayerController[] players = GameObject.FindObjectsOfType<PlayerController>(true);
if (debugLogs) Debug.Log($"[SettingsService] ApplyGameplay. mouseSensitivity={g.mouseSensitivity} players={players.Length}");
for (int i = 0; i < players.Length; i++)
{
PlayerController p = players[i];
if (p == null) { continue; }
p.SetMouseSensitivity(g.mouseSensitivity);
}
LLMChatManager[] managers = GameObject.FindObjectsOfType<LLMChatManager>(true);
for (int i = 0; i < managers.Length; i++)
{
var m = managers[i];
if (m == null) { continue; }
m.SetProviderFromSettingsIndex(g.llmProvider);
}
}
private void SaveToDisk(SettingsData data)
{
try
{
string json = JsonUtility.ToJson(data, true);
File.WriteAllText(FilePath, json);
}
catch (Exception e)
{
if (debugLogs) Debug.LogError($"[SettingsService] Failed to save settings. file={FilePath} err={e.GetType().Name}:{e.Message}");
}
}
private void ApplyGraphics(SettingsData.GraphicsSettings g)
{
if (g == null) { return; }
int previousQuality = QualitySettings.GetQualityLevel();
if (g.qualityLevel >= 0 && g.qualityLevel < QualitySettings.names.Length)
{
QualitySettings.SetQualityLevel(g.qualityLevel, true);
}
if (!IsCurrentRenderPipelineUsable())
{
if (debugLogs) Debug.LogError("[SettingsService] Render pipeline not usable after quality change. Rolling back quality.");
QualitySettings.SetQualityLevel(previousQuality, true);
}
QualitySettings.vSyncCount = g.vSync ? 1 : 0;
Application.targetFrameRate = g.targetFps <= 0 ? -1 : g.targetFps;
FullScreenMode targetMode = (FullScreenMode)Mathf.Clamp(g.fullscreenMode, (int)FullScreenMode.ExclusiveFullScreen, (int)FullScreenMode.Windowed);
int targetWidth = 0;
int targetHeight = 0;
if (g.resolutionWidth > 0 && g.resolutionHeight > 0)
{
targetWidth = g.resolutionWidth;
targetHeight = g.resolutionHeight;
}
else if (g.resolutionIndex >= 0)
{
Resolution[] resolutions = Screen.resolutions;
if (g.resolutionIndex >= 0 && g.resolutionIndex < resolutions.Length)
{
Resolution r = resolutions[g.resolutionIndex];
targetWidth = r.width;
targetHeight = r.height;
}
}
if (targetWidth <= 0 || targetHeight <= 0)
{
targetWidth = Screen.width;
targetHeight = Screen.height;
}
Screen.SetResolution(targetWidth, targetHeight, targetMode);
Screen.fullScreenMode = targetMode;
Screen.fullScreen = targetMode != FullScreenMode.Windowed;
QualitySettings.shadowDistance = Mathf.Clamp(g.shadowDistance, 0f, 500f);
#if UNITY_2022_2_OR_NEWER
QualitySettings.globalTextureMipmapLimit = Mathf.Clamp(g.textureMipmapLimit, 0, 3);
#else
QualitySettings.masterTextureLimit = Mathf.Clamp(g.textureMipmapLimit, 0, 3);
#endif
RenderPipelineAsset rp = GraphicsSettings.currentRenderPipeline;
if (rp is UniversalRenderPipelineAsset urp)
{
if (Application.isPlaying)
{
urp.renderScale = Mathf.Clamp(g.renderScale, 0.1f, 2f);
urp.msaaSampleCount = NormalizeMsaa(g.msaaSampleCount);
}
}
}
private void MigrateLegacyResolution(SettingsData data)
{
if (data == null || data.graphics == null) { return; }
if (data.graphics.resolutionWidth > 0 && data.graphics.resolutionHeight > 0) { return; }
if (data.graphics.resolutionIndex < 0) { return; }
Resolution[] resolutions = Screen.resolutions;
if (data.graphics.resolutionIndex < 0 || data.graphics.resolutionIndex >= resolutions.Length) { return; }
Resolution r = resolutions[data.graphics.resolutionIndex];
data.graphics.resolutionWidth = r.width;
data.graphics.resolutionHeight = r.height;
#if UNITY_2022_2_OR_NEWER
data.graphics.resolutionRefreshRate = Mathf.RoundToInt((float)r.refreshRateRatio.value);
#else
data.graphics.resolutionRefreshRate = r.refreshRate;
#endif
data.graphics.resolutionIndex = -1;
}
private bool IsCurrentRenderPipelineUsable()
{
RenderPipelineAsset rp = GraphicsSettings.currentRenderPipeline;
if (rp == null) { return false; }
if (rp is UniversalRenderPipelineAsset urp)
{
MethodInfo mi = typeof(UniversalRenderPipelineAsset).GetMethod("GetRenderer", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
if (mi != null)
{
try
{
object renderer = mi.GetParameters().Length == 1 ? mi.Invoke(urp, new object[] { 0 }) : mi.Invoke(urp, Array.Empty<object>());
return renderer != null;
}
catch
{
return false;
}
}
}
return true;
}
private bool Sanitize(SettingsData data)
{
if (data == null) { return false; }
bool changed = false;
if (data.graphics == null) { data.graphics = SettingsData.GraphicsSettings.Default(); changed = true; }
if (data.audio == null) { data.audio = SettingsData.AudioSettings.Default(); changed = true; }
if (data.gameplay == null) { data.gameplay = SettingsData.GameplaySettings.Default(); changed = true; }
if (data.graphics.resolutionWidth < 0) { data.graphics.resolutionWidth = 0; changed = true; }
if (data.graphics.resolutionHeight < 0) { data.graphics.resolutionHeight = 0; changed = true; }
if (data.graphics.resolutionRefreshRate < 0) { data.graphics.resolutionRefreshRate = 0; changed = true; }
if ((data.graphics.resolutionWidth > 0) != (data.graphics.resolutionHeight > 0))
{
data.graphics.resolutionWidth = 0;
data.graphics.resolutionHeight = 0;
data.graphics.resolutionRefreshRate = 0;
changed = true;
}
float clampedRenderScale = Mathf.Clamp(data.graphics.renderScale, 0.1f, 2f);
if (!Mathf.Approximately(clampedRenderScale, data.graphics.renderScale)) { data.graphics.renderScale = clampedRenderScale; changed = true; }
float clampedShadowDistance = Mathf.Clamp(data.graphics.shadowDistance, 0f, 500f);
if (!Mathf.Approximately(clampedShadowDistance, data.graphics.shadowDistance)) { data.graphics.shadowDistance = clampedShadowDistance; changed = true; }
int clampedTargetFps = data.graphics.targetFps <= 0 ? 60 : Mathf.Clamp(data.graphics.targetFps, 1, 240);
if (clampedTargetFps != data.graphics.targetFps) { data.graphics.targetFps = clampedTargetFps; changed = true; }
int clampedTextureLimit = Mathf.Clamp(data.graphics.textureMipmapLimit, 0, 3);
if (clampedTextureLimit != data.graphics.textureMipmapLimit) { data.graphics.textureMipmapLimit = clampedTextureLimit; changed = true; }
int normalizedMsaa = NormalizeMsaa(data.graphics.msaaSampleCount);
if (normalizedMsaa != data.graphics.msaaSampleCount) { data.graphics.msaaSampleCount = normalizedMsaa; changed = true; }
data.audio.master = Mathf.Clamp01(data.audio.master);
data.audio.music = Mathf.Clamp01(data.audio.music);
data.audio.sfx = Mathf.Clamp01(data.audio.sfx);
data.gameplay.mouseSensitivity = Mathf.Clamp(data.gameplay.mouseSensitivity, 0.1f, 10f);
return changed;
}
private int NormalizeMsaa(int value)
{
if (value >= 8) { return 8; }
if (value >= 4) { return 4; }
if (value >= 2) { return 2; }
return 1;
}
private void ApplyAudio(SettingsData.AudioSettings a)
{
if (a == null) { return; }
if (audioMixer != null)
{
if (debugLogs)
{
bool okMaster = audioMixer.GetFloat(masterVolumeParam, out _);
bool okMusic = audioMixer.GetFloat(musicVolumeParam, out _);
bool okSfx = audioMixer.GetFloat(sfxVolumeParam, out _);
if (!okMaster || !okMusic || !okSfx)
{
Debug.LogError($"[SettingsService] AudioMixer param missing. master={masterVolumeParam} ok={okMaster} music={musicVolumeParam} ok={okMusic} sfx={sfxVolumeParam} ok={okSfx}");
}
}
audioMixer.SetFloat(masterVolumeParam, LinearToDb(a.master));
audioMixer.SetFloat(musicVolumeParam, LinearToDb(a.music));
audioMixer.SetFloat(sfxVolumeParam, LinearToDb(a.sfx));
return;
}
AudioListener.volume = Mathf.Clamp01(a.master);
}
private float LinearToDb(float linear)
{
float v = Mathf.Clamp(linear, 0.0001f, 1f);
return Mathf.Log10(v) * 20f;
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 59953bc808e9aad44bdc3e92c7cf0509
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,9 @@
fileFormatVersion: 2
guid: 1d0c9b8a7f6e5d4c3b2a190807060504
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,9 @@
fileFormatVersion: 2
guid: 6c5b4a3d2e1f0a9b8c7d6e5f4a3b2c1d
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,31 @@
using UnityEngine;
using Core.Actions;
namespace Core.TaskSystem.Actions
{
public class AddTaskAction : GameAction
{
[SerializeField] private TaskService taskService;
[SerializeField] private TaskData task;
[SerializeField] private float timeoutSeconds = -1f;
[SerializeField] private bool addOrUpdate = true;
[SerializeField] private bool resetStatusToTodo;
public override void Invoke()
{
TaskService service = taskService != null ? taskService : TaskService.Instance;
if (service == null) { return; }
if (task == null) { return; }
if (addOrUpdate)
{
service.AddOrUpdateTask(task, timeoutSeconds, resetStatusToTodo);
}
else
{
service.AddTask(task, timeoutSeconds);
}
}
}
}
@@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: 5e7d4c3b2a1f0e9d8c7b6a5f4e3d2c1b
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,21 @@
using UnityEngine;
using Core.Actions;
namespace Core.TaskSystem.Actions
{
public class RemoveTaskAction : GameAction
{
[SerializeField] private TaskService taskService;
[SerializeField] private TaskData task;
public override void Invoke()
{
TaskService service = taskService != null ? taskService : TaskService.Instance;
if (service == null) { return; }
if (task == null || string.IsNullOrWhiteSpace(task.id)) { return; }
service.RemoveTask(task.id);
}
}
}
@@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: 4b3a2c1d0e9f8a7b6c5d4e3f2a1b0c9d
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,22 @@
using UnityEngine;
using Core.Actions;
namespace Core.TaskSystem.Actions
{
public class SetTaskStatusAction : GameAction
{
[SerializeField] private TaskService taskService;
[SerializeField] private TaskData task;
[SerializeField] private TaskStatus status = TaskStatus.Completed;
public override void Invoke()
{
TaskService service = taskService != null ? taskService : TaskService.Instance;
if (service == null) { return; }
if (task == null || string.IsNullOrWhiteSpace(task.id)) { return; }
service.SetTaskStatus(task.id, status);
}
}
}
@@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: 9a8b7c6d5e4f3a2b1c0d9e8f7a6b5c4d
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,18 @@
using System;
using System.Collections.Generic;
namespace Core.TaskSystem
{
public interface ITaskService
{
event Action Changed;
IReadOnlyList<TaskEntry> Tasks { get; }
bool TryGetTask(string taskId, out TaskEntry task);
bool AddTask(TaskData data, float timeoutSeconds = -1f);
TaskEntry AddOrUpdateTask(TaskData data, float timeoutSeconds = -1f, bool resetStatusToTodo = false);
bool RemoveTask(string taskId);
bool SetTaskStatus(string taskId, TaskStatus status);
}
}
@@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: 9d1d3f9ad3c84a6dbdfac4dc96f4e1b7
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,33 @@
using System.Collections.Generic;
using UnityEngine;
namespace Core.TaskSystem
{
public class TaskBootstrap : MonoBehaviour
{
[SerializeField] private TaskService taskService;
[SerializeField] private bool clearOnStart;
[SerializeField] private List<TaskData> tasksToAdd = new List<TaskData>();
[SerializeField] private float timeoutSeconds = -1f;
void Start()
{
TaskService service = taskService != null ? taskService : TaskService.Instance;
if (service == null) { return; }
if (clearOnStart)
{
List<TaskEntry> snapshot = new List<TaskEntry>(service.Tasks);
for (int i = 0; i < snapshot.Count; i++) { service.RemoveTask(snapshot[i].id); }
}
for (int i = 0; i < tasksToAdd.Count; i++)
{
TaskData task = tasksToAdd[i];
if (task == null) { continue; }
service.AddOrUpdateTask(task, timeoutSeconds, resetStatusToTodo: true);
}
}
}
}
@@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: 2c6a2b9e7f3d4e1d9f4b2e5a9c1d0b3f
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,13 @@
using UnityEngine;
namespace Core.TaskSystem
{
[CreateAssetMenu(menuName = "TaskSystem/Task Data")]
public class TaskData : ScriptableObject
{
public string id;
public string shortName;
[TextArea(3, 10)] public string description;
}
}
@@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: 6a5c4f2d8aa44b5d8f5e3b29f2f8bfe9
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,21 @@
using UnityEngine;
namespace Core.TaskSystem
{
[System.Serializable]
public class TaskEntry
{
public string id;
public TaskData data;
public TaskStatus status;
public float addedAt;
public float expiresAt;
public float scheduledRemoveAt;
public string ShortName => data != null && !string.IsNullOrWhiteSpace(data.shortName) ? data.shortName : id;
public string Description => data != null ? data.description : string.Empty;
public bool HasExpiry => expiresAt > 0f;
public bool HasScheduledRemoval => scheduledRemoveAt > 0f;
}
}
@@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: 3adbe6d7cf7f4a29b8d40f807a7c7e8d
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,165 @@
using System;
using System.Collections.Generic;
using UnityEngine;
namespace Core.TaskSystem
{
public class TaskService : MonoBehaviour, ITaskService
{
public static TaskService Instance { get; private set; }
[Header("Lifetime")]
[SerializeField] private bool dontDestroyOnLoad = true;
[Header("Timing")]
[SerializeField] private bool useUnscaledTime = true;
[Header("Auto Removal (seconds, <0 disables)")]
[SerializeField] private float autoRemoveCompletedAfterSeconds = -1f;
[SerializeField] private float autoRemoveExpiredAfterSeconds = -1f;
public event Action Changed;
public event Action<TaskEntry> TaskAdded;
public event Action<TaskEntry> TaskUpdated;
public event Action<TaskEntry> TaskRemoved;
public event Action<TaskEntry> TaskStatusChanged;
public IReadOnlyList<TaskEntry> Tasks => tasks;
readonly List<TaskEntry> tasks = new List<TaskEntry>();
readonly Dictionary<string, TaskEntry> tasksById = new Dictionary<string, TaskEntry>();
float Now => useUnscaledTime ? Time.unscaledTime : Time.time;
void Awake()
{
if (Instance != null && Instance != this)
{
Destroy(gameObject);
return;
}
Instance = this;
if (dontDestroyOnLoad) { DontDestroyOnLoad(gameObject); }
}
void Update()
{
if (tasks.Count == 0) { return; }
float now = Now;
for (int i = 0; i < tasks.Count; i++)
{
TaskEntry task = tasks[i];
if (task.status != TaskStatus.Todo) { continue; }
if (!task.HasExpiry) { continue; }
if (now < task.expiresAt) { continue; }
SetTaskStatus(task.id, TaskStatus.Expired);
}
for (int i = tasks.Count - 1; i >= 0; i--)
{
TaskEntry task = tasks[i];
if (!task.HasScheduledRemoval) { continue; }
if (now < task.scheduledRemoveAt) { continue; }
RemoveTask(task.id);
}
}
public bool TryGetTask(string taskId, out TaskEntry task)
{
if (string.IsNullOrWhiteSpace(taskId))
{
task = null;
return false;
}
return tasksById.TryGetValue(taskId, out task);
}
public bool AddTask(TaskData data, float timeoutSeconds = -1f)
{
if (data == null || string.IsNullOrWhiteSpace(data.id)) { return false; }
if (tasksById.ContainsKey(data.id)) { return false; }
TaskEntry task = new TaskEntry
{
id = data.id,
data = data,
status = TaskStatus.Todo,
addedAt = Now,
expiresAt = timeoutSeconds > 0f ? Now + timeoutSeconds : 0f,
scheduledRemoveAt = 0f
};
tasks.Add(task);
tasksById.Add(task.id, task);
TaskAdded?.Invoke(task);
Changed?.Invoke();
return true;
}
public TaskEntry AddOrUpdateTask(TaskData data, float timeoutSeconds = -1f, bool resetStatusToTodo = false)
{
if (data == null || string.IsNullOrWhiteSpace(data.id)) { return null; }
if (!tasksById.TryGetValue(data.id, out TaskEntry task))
{
AddTask(data, timeoutSeconds);
tasksById.TryGetValue(data.id, out TaskEntry added);
return added;
}
task.data = data;
if (timeoutSeconds > 0f) { task.expiresAt = Now + timeoutSeconds; }
if (resetStatusToTodo)
{
task.status = TaskStatus.Todo;
task.scheduledRemoveAt = 0f;
}
TaskUpdated?.Invoke(task);
Changed?.Invoke();
return task;
}
public bool RemoveTask(string taskId)
{
if (!tasksById.TryGetValue(taskId, out TaskEntry task)) { return false; }
tasksById.Remove(taskId);
tasks.Remove(task);
TaskRemoved?.Invoke(task);
Changed?.Invoke();
return true;
}
public bool SetTaskStatus(string taskId, TaskStatus status)
{
if (!tasksById.TryGetValue(taskId, out TaskEntry task)) { return false; }
if (task.status == status) { return true; }
task.status = status;
float autoRemoveDelay = status switch
{
TaskStatus.Completed => autoRemoveCompletedAfterSeconds,
TaskStatus.Expired => autoRemoveExpiredAfterSeconds,
_ => -1f
};
task.scheduledRemoveAt = autoRemoveDelay > 0f ? Now + autoRemoveDelay : 0f;
TaskStatusChanged?.Invoke(task);
TaskUpdated?.Invoke(task);
Changed?.Invoke();
return true;
}
}
}
@@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: a4b0f1c9b25b4c0c9c89a3e3e4f53b1f
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,10 @@
namespace Core.TaskSystem
{
public enum TaskStatus
{
Todo,
Completed,
Expired
}
}
@@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: 3e6a7e7f3eea4d3b8bf42b1d6e6d3c41
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: f60264dbdddbaae4b9aa57c4ce03b611
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,43 @@
using UnityEngine;
using UnityEngine.Events;
namespace Core.Triggers
{
public class InteractFailedTrigger : MonoBehaviour
{
[SerializeField] private bool debugLog;
[Tooltip("If true, the trigger will only execute its action once.")]
[SerializeField] private bool onlyOnce = true;
[Tooltip("Optional: Only trigger if the failure reason matches this exact string. Leave empty to trigger on any failure.")]
[SerializeField] private string targetFailReason = "";
[Tooltip("Event invoked when an interaction fails.")]
[SerializeField] private UnityEvent onInteractFailedEvent;
private bool hasTriggered = false;
public void OnFailed(string failReason)
{
if (debugLog) Debug.Log($"[InteractFailedTrigger] OnFailed '{name}' reason='{failReason}'", this);
if (onlyOnce && hasTriggered) return;
if (string.IsNullOrEmpty(targetFailReason) || targetFailReason == failReason)
{
hasTriggered = true;
onInteractFailedEvent?.Invoke();
if (debugLog) Debug.Log($"[InteractFailedTrigger] Invoked '{name}'", this);
return;
}
if (debugLog) Debug.Log($"[InteractFailedTrigger] Filtered '{name}' targetFailReason='{targetFailReason}'", this);
}
public void ResetTrigger()
{
hasTriggered = false;
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 36d9baec5bc6bf649a071c7ecf375d3a
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,35 @@
using Core.SaveSystem;
using UnityEngine;
using UnityEngine.Events;
namespace Core.Triggers
{
public class OneShotFlagEvent : MonoBehaviour
{
[SerializeField] private string flagId;
[SerializeField] private bool autoSaveAfterTrigger = true;
[SerializeField] private UnityEvent onFirstTriggered;
[SerializeField] private UnityEvent onAlreadyTriggered;
public void Trigger()
{
if (PersistentFlagsService.Instance == null) { return; }
if (string.IsNullOrWhiteSpace(flagId)) { return; }
bool first = PersistentFlagsService.Instance.TrySet(flagId);
if (first)
{
onFirstTriggered?.Invoke();
if (autoSaveAfterTrigger)
{
PersistentFlagsService.Instance.SaveNow();
}
}
else
{
onAlreadyTriggered?.Invoke();
}
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 416e2b00557dcfa498dad8105d8dbbe3
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,36 @@
using UnityEngine;
using UnityEngine.Events;
namespace Core.Triggers
{
[RequireComponent(typeof(Collider))]
public class TriggerVolume : MonoBehaviour
{
[Tooltip("The tag required on the entering collider to trigger this event (leave empty to allow any).")]
[SerializeField] private string targetTag = "Player";
[Tooltip("If true, the trigger will only execute its action once.")]
[SerializeField] private bool onlyOnce = true;
[Tooltip("Event invoked when a valid object enters the trigger.")]
[SerializeField] private UnityEvent onTriggerEnterEvent;
private bool hasTriggered = false;
private void OnTriggerEnter(Collider other)
{
if (onlyOnce && hasTriggered) return;
if (string.IsNullOrEmpty(targetTag) || other.CompareTag(targetTag))
{
hasTriggered = true;
onTriggerEnterEvent?.Invoke();
}
}
public void ResetTrigger()
{
hasTriggered = false;
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: d1b568f348018674cbc63d253cb67b67
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+8
View File
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 8acc939735b318b4f8c042f3be097934
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,28 @@
using UnityEngine;
using UnityEditor;
using System.IO;
public class SaveDataCleaner : EditorWindow
{
[MenuItem("Tools/Clear Save Data")]
public static void ClearSaveData()
{
string savePath = Path.Combine(Application.persistentDataPath, "savegame.json");
if (File.Exists(savePath))
{
File.Delete(savePath);
Debug.Log($"[SaveDataCleaner] 已删除存档文件: {savePath}");
}
else
{
Debug.LogWarning("[SaveDataCleaner] 未找到存档文件,无需清理。");
}
// 还可以清理 PlayerPrefs (如果有用到)
// PlayerPrefs.DeleteAll();
// Debug.Log("[SaveDataCleaner] 已清理 PlayerPrefs。");
AssetDatabase.Refresh();
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: b5a26c118a2e0d44587e95e4b664480d
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 6330bcdbe15afec4cad180985c05dc81
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,158 @@
using System.Collections;
using UnityEngine;
#if DOTWEEN
using DG.Tweening;
#endif
namespace EndingSystem
{
public class EndingDoorController : MonoBehaviour
{
[Header("Door References")]
public Transform leftDoor;
public Transform rightDoor;
[Header("Animation Settings")]
[Tooltip("The initial small angle to open the door (e.g., crack it open slightly)")]
public float openLittleAngle = 10f;
public float openLittleDuration = 1.5f;
[Tooltip("How long to pause before fully opening the door")]
public float pauseDuration = 0.5f;
[Tooltip("The final angle to fully open the door")]
public float fullOpenAngle = 90f;
public float fullOpenDuration = 3f;
[Tooltip("The axis of rotation for the doors (usually Vector3.up)")]
public Vector3 rotationAxis = Vector3.up;
[Tooltip("If true, the right door rotates in the opposite direction of the left door")]
public bool mirrorRightDoor = true;
#if DOTWEEN
[Header("DOTween")]
public Ease openLittleEase = Ease.InOutSine;
public Ease fullOpenEase = Ease.InOutSine;
#endif
private Quaternion leftClosedRotation;
private Quaternion rightClosedRotation;
#if DOTWEEN
private Sequence currentSequence;
#endif
public void OpenDoor(bool useUnscaledTime = false)
{
#if DOTWEEN
PlayOpenDoor(useUnscaledTime);
#else
StartCoroutine(DoorAnimationCoroutine());
#endif
}
#if DOTWEEN
private void OnDisable()
{
if (currentSequence != null)
{
currentSequence.Kill(false);
currentSequence = null;
}
}
private void CacheClosedRotations()
{
if (leftDoor != null) leftClosedRotation = leftDoor.localRotation;
if (rightDoor != null) rightClosedRotation = rightDoor.localRotation;
}
private void PlayOpenDoor(bool useUnscaledTime)
{
CacheClosedRotations();
if (leftDoor != null) leftDoor.DOKill();
if (rightDoor != null) rightDoor.DOKill();
if (currentSequence != null)
{
currentSequence.Kill(false);
currentSequence = null;
}
currentSequence = DOTween.Sequence();
currentSequence.SetUpdate(useUnscaledTime);
currentSequence.Append(CreateRotateTween(openLittleAngle, Mathf.Max(0.0001f, openLittleDuration), openLittleEase, useUnscaledTime));
if (pauseDuration > 0f) currentSequence.AppendInterval(pauseDuration);
currentSequence.Append(CreateRotateTween(fullOpenAngle, Mathf.Max(0.0001f, fullOpenDuration), fullOpenEase, useUnscaledTime));
}
private Tween CreateRotateTween(float angle, float duration, Ease ease, bool useUnscaledTime)
{
Sequence seq = DOTween.Sequence();
seq.SetUpdate(useUnscaledTime);
Vector3 axis = rotationAxis.sqrMagnitude > 0f ? rotationAxis.normalized : Vector3.up;
if (leftDoor != null)
{
Quaternion target = leftClosedRotation * Quaternion.AngleAxis(angle, axis);
Tween t = leftDoor.DOLocalRotateQuaternion(target, duration).SetEase(ease).SetUpdate(useUnscaledTime);
seq.Join(t);
}
if (rightDoor != null)
{
float rightAngle = mirrorRightDoor ? -angle : angle;
Quaternion target = rightClosedRotation * Quaternion.AngleAxis(rightAngle, axis);
Tween t = rightDoor.DOLocalRotateQuaternion(target, duration).SetEase(ease).SetUpdate(useUnscaledTime);
seq.Join(t);
}
return seq;
}
#endif
private IEnumerator DoorAnimationCoroutine()
{
yield return StartCoroutine(RotateDoors(0f, openLittleAngle, openLittleDuration));
yield return new WaitForSeconds(pauseDuration);
yield return StartCoroutine(RotateDoors(openLittleAngle, fullOpenAngle, fullOpenDuration));
}
private IEnumerator RotateDoors(float startAngle, float endAngle, float duration)
{
float t = 0f;
while (t < 1f)
{
t += Time.deltaTime / duration;
float currentAngle = Mathf.Lerp(startAngle, endAngle, t);
if (leftDoor != null)
{
leftDoor.localRotation = Quaternion.AngleAxis(currentAngle, rotationAxis);
}
if (rightDoor != null)
{
float rightAngle = mirrorRightDoor ? -currentAngle : currentAngle;
rightDoor.localRotation = Quaternion.AngleAxis(rightAngle, rotationAxis);
}
yield return null;
}
if (leftDoor != null)
leftDoor.localRotation = Quaternion.AngleAxis(endAngle, rotationAxis);
if (rightDoor != null)
{
float rightAngleFinal = mirrorRightDoor ? -endAngle : endAngle;
rightDoor.localRotation = Quaternion.AngleAxis(rightAngleFinal, rotationAxis);
}
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 4b39d9923eb612140b7809b1cad268e3
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,88 @@
using System.Collections;
using UnityEngine;
#if DOTWEEN
using DG.Tweening;
#endif
namespace EndingSystem
{
public class EndingLightController : MonoBehaviour
{
[Header("Light Reference")]
public Light targetLight;
[Header("Animation Settings")]
[Tooltip("The final intensity of the light when fully turned on")]
public float targetIntensity = 5f;
[Tooltip("How long it takes for the light to reach target intensity")]
public float fadeDuration = 3f;
#if DOTWEEN
[Header("DOTween")]
public Ease fadeEase = Ease.InOutSine;
private Tween currentTween;
#endif
private void Awake()
{
if (targetLight != null)
{
targetLight.intensity = 0f;
targetLight.enabled = true;
}
}
public void TurnOnLight(bool useUnscaledTime = false)
{
if (targetLight != null)
{
#if DOTWEEN
PlayFade(useUnscaledTime);
#else
StartCoroutine(FadeLightCoroutine());
#endif
}
}
#if DOTWEEN
private void OnDisable()
{
if (currentTween != null)
{
currentTween.Kill(false);
currentTween = null;
}
}
private void PlayFade(bool useUnscaledTime)
{
if (currentTween != null)
{
currentTween.Kill(false);
currentTween = null;
}
float duration = Mathf.Max(0.0001f, fadeDuration);
currentTween = DOTween.To(() => targetLight.intensity, v => targetLight.intensity = v, targetIntensity, duration)
.SetEase(fadeEase)
.SetTarget(targetLight)
.SetUpdate(useUnscaledTime);
}
#endif
private IEnumerator FadeLightCoroutine()
{
float t = 0f;
float startIntensity = targetLight.intensity;
while (t < 1f)
{
t += Time.deltaTime / fadeDuration;
targetLight.intensity = Mathf.Lerp(startIntensity, targetIntensity, t);
yield return null;
}
targetLight.intensity = targetIntensity;
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 0cdac34e0f9cd2f4cbd68a80a309d726
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,382 @@
using System.Collections;
using UnityEngine;
using Core.InputLock;
#if DOTWEEN
using DG.Tweening;
#endif
namespace EndingSystem
{
public class EndingSequenceController : MonoBehaviour
{
[Header("Overlay Safety")]
public bool forceScreenObjectsActive = true;
public bool bringScreenToFront = true;
public bool overrideCanvasSorting = true;
public int overlaySortingOrder = 5000;
[Header("Sequence Settings")]
public float delayBeforeFadeFromBlack = 1f;
[Header("UI Fader")]
[Tooltip("Assign a CanvasGroup attached to a black Image that covers the whole screen")]
public CanvasGroup blackScreenCanvasGroup;
public float fadeToBlackDuration = 1.5f;
public float fadeFromBlackDuration = 2f;
[Tooltip("Assign a CanvasGroup attached to a white Image that covers the whole screen")]
public CanvasGroup whiteScreenCanvasGroup;
public float fadeToWhiteDuration = 2f;
[Tooltip("How long to wait after the door opens before fading to white")]
public float delayBeforeFadeToWhite = 2f;
[Header("Ending UI")]
[Tooltip("The UI Panel to activate at the very end (e.g. Return to Menu / Credits)")]
public GameObject endingUIPanel;
[Header("Player & Camera References")]
[Tooltip("The parent object of the player (the one with CharacterController)")]
public Transform playerBody;
[Tooltip("The camera pivot (usually the parent of the camera that handles up/down look)")]
public Transform playerCameraPivot;
public Camera playerCamera;
[Header("Target Positions & Angles")]
[Tooltip("Where should the player be teleported to when the screen goes black?")]
public Transform targetPlayerTransform;
[Tooltip("The angle to which the camera will look up (negative values usually mean looking up)")]
public float cameraLookUpAngle = -30f;
public float cameraLookDuration = 3f;
[Tooltip("Target FOV to focus on the door")]
public float targetFOV = 30f;
public float fovShrinkDuration = 2f;
[Header("Scene Object Controllers")]
public EndingDoorController doorController;
public EndingLightController lightController;
#if DOTWEEN
[Header("DOTween")]
public bool useUnscaledTime = false;
public Ease fadeToBlackEase = Ease.InOutSine;
public Ease fadeFromBlackEase = Ease.InOutSine;
public Ease fadeToWhiteEase = Ease.InOutSine;
public Ease lookEase = Ease.InOutSine;
public Ease fovEase = Ease.InOutSine;
#endif
private bool sequenceStarted = false;
#if DOTWEEN
private Sequence sequence;
#endif
public void StartEndingSequence()
{
if (sequenceStarted) return;
sequenceStarted = true;
#if DOTWEEN
PlaySequenceWithDOTween();
#else
StartCoroutine(EndingCoroutine());
#endif
}
#if DOTWEEN
private void OnDisable()
{
if (sequence != null)
{
sequence.Kill(false);
sequence = null;
}
}
private void PlaySequenceWithDOTween()
{
if (sequence != null)
{
sequence.Kill(false);
sequence = null;
}
if (PlayerControlLockService.Instance != null)
{
PlayerControlLockService.Instance.Lock(this);
}
if (blackScreenCanvasGroup == null)
{
Debug.LogWarning("[EndingSequenceController] Black Screen CanvasGroup is not assigned!");
return;
}
if (endingUIPanel != null)
{
endingUIPanel.SetActive(false);
}
PrepareOverlay(blackScreenCanvasGroup, setAlpha: true, alpha: 0f);
if (whiteScreenCanvasGroup != null)
{
PrepareOverlay(whiteScreenCanvasGroup, setAlpha: true, alpha: 0f);
}
blackScreenCanvasGroup.DOKill();
if (playerCameraPivot != null) playerCameraPivot.DOKill();
if (playerCamera != null) DOTween.Kill(playerCamera);
sequence = DOTween.Sequence();
sequence.SetUpdate(useUnscaledTime);
sequence.Append(blackScreenCanvasGroup.DOFade(1f, Mathf.Max(0.0001f, fadeToBlackDuration)).SetEase(fadeToBlackEase));
sequence.AppendCallback(ApplyBlackScreenPlacementAndReset);
if (delayBeforeFadeFromBlack > 0f)
{
sequence.AppendInterval(delayBeforeFadeFromBlack);
}
Tween fadeFromTween = blackScreenCanvasGroup.DOFade(0f, Mathf.Max(0.0001f, fadeFromBlackDuration)).SetEase(fadeFromBlackEase);
sequence.Append(fadeFromTween);
if (playerCameraPivot != null)
{
Vector3 targetEuler = new Vector3(cameraLookUpAngle, 0f, 0f);
Tween lookTween = playerCameraPivot.DOLocalRotate(targetEuler, Mathf.Max(0.0001f, cameraLookDuration), RotateMode.Fast).SetEase(lookEase);
lookTween.SetUpdate(useUnscaledTime);
sequence.Join(lookTween);
}
if (playerCamera != null)
{
float duration = Mathf.Max(0.0001f, fovShrinkDuration);
Tween fovTween = DOTween.To(() => playerCamera.fieldOfView, v => playerCamera.fieldOfView = v, targetFOV, duration)
.SetEase(fovEase)
.SetTarget(playerCamera)
.SetUpdate(useUnscaledTime);
sequence.Append(fovTween);
}
sequence.AppendCallback(() =>
{
if (doorController != null) doorController.OpenDoor(useUnscaledTime);
if (lightController != null) lightController.TurnOnLight(useUnscaledTime);
});
// 等待开门动画和光照完成,再等一个自定义时间
float doorWaitTime = (doorController != null) ? (doorController.openLittleDuration + doorController.pauseDuration + doorController.fullOpenDuration) : 0f;
sequence.AppendInterval(doorWaitTime + delayBeforeFadeToWhite);
// 淡入白屏
if (whiteScreenCanvasGroup != null)
{
Tween whiteFadeTween = whiteScreenCanvasGroup.DOFade(1f, Mathf.Max(0.0001f, fadeToWhiteDuration)).SetEase(fadeToWhiteEase);
sequence.Append(whiteFadeTween);
}
// 最后激活UI并解锁鼠标
sequence.AppendCallback(FinishSequence);
}
#endif
private IEnumerator EndingCoroutine()
{
if (PlayerControlLockService.Instance != null)
{
PlayerControlLockService.Instance.Lock(this);
}
if (endingUIPanel != null)
{
endingUIPanel.SetActive(false);
}
PrepareOverlay(blackScreenCanvasGroup, setAlpha: true, alpha: 0f);
if (whiteScreenCanvasGroup != null) PrepareOverlay(whiteScreenCanvasGroup, setAlpha: true, alpha: 0f);
yield return StartCoroutine(FadeScreen(0f, 1f, fadeToBlackDuration, blackScreenCanvasGroup));
ApplyBlackScreenPlacementAndReset();
yield return new WaitForSeconds(delayBeforeFadeFromBlack);
Coroutine fadeRoutine = StartCoroutine(FadeScreen(1f, 0f, fadeFromBlackDuration, blackScreenCanvasGroup));
Coroutine lookRoutine = StartCoroutine(LookUpCoroutine());
yield return fadeRoutine;
yield return lookRoutine;
if (playerCamera != null)
{
float startFOV = playerCamera.fieldOfView;
float t = 0f;
while (t < 1f)
{
t += Time.deltaTime / fovShrinkDuration;
playerCamera.fieldOfView = Mathf.Lerp(startFOV, targetFOV, t);
yield return null;
}
playerCamera.fieldOfView = targetFOV;
}
if (doorController != null) doorController.OpenDoor();
if (lightController != null) lightController.TurnOnLight();
// 等待开门时间
float doorWaitTime = (doorController != null) ? (doorController.openLittleDuration + doorController.pauseDuration + doorController.fullOpenDuration) : 0f;
yield return new WaitForSeconds(doorWaitTime + delayBeforeFadeToWhite);
if (whiteScreenCanvasGroup != null)
{
yield return StartCoroutine(FadeScreen(0f, 1f, fadeToWhiteDuration, whiteScreenCanvasGroup));
}
FinishSequence();
}
private void FinishSequence()
{
if (endingUIPanel != null)
{
endingUIPanel.SetActive(true);
BringEndingUIToFront();
}
// 解锁玩家输入,以显示鼠标点击 UI,但是玩家在结局时应该只看UI,通常我们可以保持移动锁定
// 但你的要求是"解锁鼠标",我们可以解锁 LockService,由于游戏已经结束,你可能需要在UI层面自己拦截按键
// 另外一种做法是调用 Unlock,然后让 UI 自己去 Lock(this),这取决于你的 UI 设计
if (PlayerControlLockService.Instance != null)
{
PlayerControlLockService.Instance.Unlock(this);
}
// 强制显示鼠标
Cursor.lockState = CursorLockMode.None;
Cursor.visible = true;
}
private void BringEndingUIToFront()
{
if (endingUIPanel == null) { return; }
endingUIPanel.transform.SetAsLastSibling();
Canvas uiCanvas = endingUIPanel.GetComponentInParent<Canvas>(true);
if (uiCanvas == null) { return; }
int maxOverlayOrder = -1;
Canvas overlayCanvas = null;
if (blackScreenCanvasGroup != null) overlayCanvas = blackScreenCanvasGroup.GetComponentInParent<Canvas>(true);
if (overlayCanvas == null && whiteScreenCanvasGroup != null) overlayCanvas = whiteScreenCanvasGroup.GetComponentInParent<Canvas>(true);
if (overlayCanvas != null) maxOverlayOrder = overlayCanvas.sortingOrder;
if (uiCanvas == overlayCanvas)
{
if (blackScreenCanvasGroup != null) blackScreenCanvasGroup.transform.SetSiblingIndex(0);
if (whiteScreenCanvasGroup != null) whiteScreenCanvasGroup.transform.SetSiblingIndex(0);
endingUIPanel.transform.SetAsLastSibling();
return;
}
uiCanvas.overrideSorting = true;
uiCanvas.sortingOrder = Mathf.Max(uiCanvas.sortingOrder, maxOverlayOrder + 1, overlaySortingOrder + 1);
}
private void ApplyBlackScreenPlacementAndReset()
{
if (playerBody != null && targetPlayerTransform != null)
{
var cc = playerBody.GetComponent<CharacterController>();
if (cc != null) cc.enabled = false;
playerBody.position = targetPlayerTransform.position;
playerBody.rotation = targetPlayerTransform.rotation;
if (cc != null) cc.enabled = true;
}
if (playerCameraPivot != null)
{
playerCameraPivot.localRotation = Quaternion.identity;
}
}
private IEnumerator LookUpCoroutine()
{
if (playerCameraPivot == null) yield break;
Quaternion startRot = playerCameraPivot.localRotation;
Quaternion endRot = Quaternion.Euler(cameraLookUpAngle, 0f, 0f);
float t = 0f;
while (t < 1f)
{
t += Time.deltaTime / cameraLookDuration;
playerCameraPivot.localRotation = Quaternion.Lerp(startRot, endRot, t);
yield return null;
}
playerCameraPivot.localRotation = endRot;
}
private IEnumerator FadeScreen(float startAlpha, float endAlpha, float duration, CanvasGroup cg)
{
if (cg == null)
{
Debug.LogWarning("[EndingSequenceController] A CanvasGroup is not assigned for fade!");
yield break;
}
float t = 0f;
while (t < 1f)
{
t += Time.deltaTime / duration;
cg.alpha = Mathf.Lerp(startAlpha, endAlpha, t);
yield return null;
}
cg.alpha = endAlpha;
}
private void PrepareOverlay(CanvasGroup cg, bool setAlpha, float alpha)
{
if (cg == null) { return; }
if (forceScreenObjectsActive)
{
if (!cg.gameObject.activeSelf) cg.gameObject.SetActive(true);
if (cg.transform.parent != null && !cg.transform.parent.gameObject.activeSelf) cg.transform.parent.gameObject.SetActive(true);
}
var animator = cg.GetComponent<Animator>();
if (animator != null) animator.enabled = false;
var reachCanvasGroupAnimator = cg.GetComponent<Michsky.UI.Reach.CanvasGroupAnimator>();
if (reachCanvasGroupAnimator != null) reachCanvasGroupAnimator.enabled = false;
var reachImageFading = cg.GetComponent<Michsky.UI.Reach.ImageFading>();
if (reachImageFading != null) reachImageFading.enabled = false;
#if DOTWEEN
cg.DOKill();
#endif
cg.interactable = false;
cg.blocksRaycasts = false;
if (setAlpha) cg.alpha = alpha;
if (bringScreenToFront)
{
cg.transform.SetAsLastSibling();
}
if (overrideCanvasSorting)
{
Canvas canvas = cg.GetComponentInParent<Canvas>(true);
if (canvas != null)
{
canvas.overrideSorting = true;
canvas.sortingOrder = overlaySortingOrder;
}
}
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 9e117aa7f36336b4cbb9ac8c6bf7afc8
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,121 @@
using UnityEngine;
namespace EndingSystem
{
[RequireComponent(typeof(BoxCollider))]
public class EndingTrigger : MonoBehaviour
{
[Header("Controller Reference")]
public EndingSequenceController sequenceController;
[Header("Trigger Settings")]
[Tooltip("If true, the sequence will start when the player enters the trigger. If false, you can call TriggerSequence() manually (e.g., from a UI Button).")]
public bool triggerOnCollision = true;
[Tooltip("Tag of the player object to detect collision")]
public string playerTag = "Player";
[Header("Win Condition")]
[Tooltip("If true, collision trigger requires EndingWinConditionService.CanTriggerEnding == true")]
public bool requireWinConditionToTrigger = true;
[Tooltip("If true, TriggerSequence() will ignore win condition (useful for debug buttons)")]
public bool allowManualTriggerWhenLocked = true;
private bool hasTriggered = false;
private void OnEnable()
{
enabled = true;
triggerOnCollision = true;
requireWinConditionToTrigger = false;
hasTriggered = false;
var coll = GetComponent<BoxCollider>();
if (coll != null) coll.isTrigger = true;
}
private void Awake()
{
enabled = true;
triggerOnCollision = true;
requireWinConditionToTrigger = false;
var coll = GetComponent<BoxCollider>();
coll.isTrigger = true;
if (sequenceController == null)
{
sequenceController = GetComponentInParent<EndingSequenceController>(true);
if (sequenceController == null)
{
sequenceController = FindObjectOfType<EndingSequenceController>(true);
}
}
}
private void OnValidate()
{
enabled = true;
triggerOnCollision = true;
requireWinConditionToTrigger = false;
var coll = GetComponent<BoxCollider>();
if (coll != null) coll.isTrigger = true;
if (sequenceController == null)
{
sequenceController = GetComponentInParent<EndingSequenceController>(true);
if (sequenceController == null)
{
sequenceController = FindObjectOfType<EndingSequenceController>(true);
}
}
}
private void OnTriggerEnter(Collider other)
{
if (hasTriggered) return;
if (other.CompareTag(playerTag))
{
TriggerSequence();
}
}
/// <summary>
/// 提供给外部调用的接口,比如 Button OnClick 事件,方便在开发阶段不检查碰撞直接触发
/// </summary>
public void TriggerSequence()
{
if (hasTriggered) return;
hasTriggered = true;
if (!allowManualTriggerWhenLocked && requireWinConditionToTrigger)
{
var wc = EndingWinConditionService.EnsureInstance();
if (wc != null && !wc.CanTriggerEnding)
{
hasTriggered = false;
return;
}
}
if (sequenceController == null)
{
sequenceController = GetComponentInParent<EndingSequenceController>(true);
if (sequenceController == null)
{
sequenceController = FindObjectOfType<EndingSequenceController>(true);
}
}
if (sequenceController != null)
{
sequenceController.StartEndingSequence();
}
else
{
Debug.LogError("[EndingTrigger] EndingSequenceController is not assigned!");
}
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 2c05e5ca4a67e0d48b7efd0317624066
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,18 @@
using UnityEngine;
namespace EndingSystem
{
public class EndingWinConditionDebugButton : MonoBehaviour
{
public void UnlockEnding()
{
EndingWinConditionService.EnsureInstance().UnlockEnding();
}
public void LockEnding()
{
EndingWinConditionService.EnsureInstance().LockEnding();
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 966a245d19c45bf408394b84389f4631
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,76 @@
using System;
using UnityEngine;
namespace EndingSystem
{
public class EndingWinConditionService : MonoBehaviour
{
public static EndingWinConditionService Instance { get; private set; }
[Header("Win Condition")]
[SerializeField] private bool canTriggerEnding = false;
[Header("Lifetime")]
[SerializeField] private bool dontDestroyOnLoad = true;
public event Action<bool> CanTriggerEndingChanged;
public bool CanTriggerEnding => canTriggerEnding;
private void Awake()
{
if (Instance != null && Instance != this)
{
Destroy(gameObject);
return;
}
Instance = this;
if (dontDestroyOnLoad) { DontDestroyOnLoad(gameObject); }
}
public void SetCanTriggerEnding(bool value)
{
if (canTriggerEnding == value) { return; }
canTriggerEnding = value;
CanTriggerEndingChanged?.Invoke(canTriggerEnding);
}
public void UnlockEnding()
{
SetCanTriggerEnding(true);
}
public void DebugUnlockEnding()
{
EnsureInstance().UnlockEnding();
}
public void LockEnding()
{
SetCanTriggerEnding(false);
}
public void DebugLockEnding()
{
EnsureInstance().LockEnding();
}
public static EndingWinConditionService EnsureInstance()
{
if (Instance != null) { return Instance; }
EndingWinConditionService existing = FindObjectOfType<EndingWinConditionService>(true);
if (existing != null)
{
Instance = existing;
return Instance;
}
var go = new GameObject("EndingWinConditionService");
var service = go.AddComponent<EndingWinConditionService>();
Instance = service;
return service;
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: a419e3badae0d7a4ea5d3b584d74148e
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,211 @@
using UnityEngine;
using Core.InputLock;
#if DOTWEEN
using DG.Tweening;
#endif
namespace EndingSystem
{
public class FaintEndingSequenceController : MonoBehaviour
{
[Header("References")]
public Player.PlayerController playerController;
public Transform cameraPivot;
public Transform cameraTransform;
public CanvasGroup blackScreenCanvasGroup;
public GameObject endingUIPanel;
[Header("Input")]
public bool keepCursorLockedUntilEnd = true;
public bool switchToUIActionMapAtEnd = true;
#if DOTWEEN
[Header("DOTween")]
public bool useUnscaledTime = false;
public Ease step1Ease = Ease.InOutSine;
public Ease step3Ease = Ease.InOutSine;
public Ease step4Ease = Ease.InOutSine;
public Ease alphaEase = Ease.InOutSine;
#endif
[Header("Step 1")]
public float step1Duration = 1.2f;
public Vector3 step1CameraLocalOffset = new Vector3(0f, -0.4f, 0.3f);
public float step1Pitch = 35f;
[Range(0f, 1f)] public float step1Alpha = 0.65f;
[Header("Step 2")]
public float step2HoldDuration = 0.4f;
[Header("Step 3")]
public float step3Duration = 0.4f;
public Vector3 step3CameraLocalOffset = new Vector3(0f, -0.32f, 0.24f);
public float step3Pitch = 25f;
[Range(0f, 1f)] public float step3Alpha = 0.45f;
[Header("Step 4")]
public float step4Duration = 1.1f;
public Vector3 step4CameraLocalOffset = new Vector3(0f, -0.9f, 0.45f);
public float step4Pitch = 90f;
[Range(0f, 1f)] public float step4Alpha = 1f;
private bool sequenceStarted;
private Vector3 initialCameraLocalPos;
private Quaternion initialPivotLocalRot;
private float initialPivotPitch;
#if DOTWEEN
private Sequence sequence;
#endif
public void StartFaintSequence()
{
if (sequenceStarted) return;
sequenceStarted = true;
if (endingUIPanel != null) endingUIPanel.SetActive(false);
if (playerController == null) playerController = FindObjectOfType<Player.PlayerController>(true);
if (playerController != null) playerController.enabled = false;
if (cameraTransform == null)
{
var cam = Camera.main;
if (cam != null) cameraTransform = cam.transform;
}
if (cameraPivot == null && cameraTransform != null) cameraPivot = cameraTransform.parent;
if (cameraTransform != null) initialCameraLocalPos = cameraTransform.localPosition;
if (cameraPivot != null)
{
initialPivotLocalRot = cameraPivot.localRotation;
float x = cameraPivot.localEulerAngles.x;
if (x > 180f) x -= 360f;
initialPivotPitch = x;
}
PrepareOverlay(blackScreenCanvasGroup, setAlpha: true, alpha: 0f);
#if DOTWEEN
PlayWithDOTween();
#else
StartCoroutine(PlayWithCoroutine());
#endif
}
#if DOTWEEN
private void OnDisable()
{
if (sequence != null)
{
sequence.Kill(false);
sequence = null;
}
}
private void PlayWithDOTween()
{
if (sequence != null)
{
sequence.Kill(false);
sequence = null;
}
if (cameraTransform != null) cameraTransform.DOKill();
if (cameraPivot != null) cameraPivot.DOKill();
sequence = DOTween.Sequence();
sequence.SetUpdate(useUnscaledTime);
Vector3 step1Pos = initialCameraLocalPos + step1CameraLocalOffset;
Vector3 step3Pos = initialCameraLocalPos + step3CameraLocalOffset;
Vector3 step4Pos = initialCameraLocalPos + step4CameraLocalOffset;
if (blackScreenCanvasGroup != null)
sequence.Join(blackScreenCanvasGroup.DOFade(step1Alpha, Mathf.Max(0.0001f, step1Duration)).SetEase(alphaEase));
if (cameraTransform != null)
sequence.Join(cameraTransform.DOLocalMove(step1Pos, Mathf.Max(0.0001f, step1Duration)).SetEase(step1Ease).SetUpdate(useUnscaledTime));
if (cameraPivot != null)
sequence.Join(cameraPivot.DOLocalRotate(new Vector3(initialPivotPitch + step1Pitch, 0f, 0f), Mathf.Max(0.0001f, step1Duration), RotateMode.Fast).SetEase(step1Ease).SetUpdate(useUnscaledTime));
if (step2HoldDuration > 0f) sequence.AppendInterval(step2HoldDuration);
if (blackScreenCanvasGroup != null)
sequence.Append(blackScreenCanvasGroup.DOFade(step3Alpha, Mathf.Max(0.0001f, step3Duration)).SetEase(alphaEase));
if (cameraTransform != null)
sequence.Join(cameraTransform.DOLocalMove(step3Pos, Mathf.Max(0.0001f, step3Duration)).SetEase(step3Ease).SetUpdate(useUnscaledTime));
if (cameraPivot != null)
sequence.Join(cameraPivot.DOLocalRotate(new Vector3(initialPivotPitch + step3Pitch, 0f, 0f), Mathf.Max(0.0001f, step3Duration), RotateMode.Fast).SetEase(step3Ease).SetUpdate(useUnscaledTime));
if (blackScreenCanvasGroup != null)
sequence.Append(blackScreenCanvasGroup.DOFade(step4Alpha, Mathf.Max(0.0001f, step4Duration)).SetEase(alphaEase));
if (cameraTransform != null)
sequence.Join(cameraTransform.DOLocalMove(step4Pos, Mathf.Max(0.0001f, step4Duration)).SetEase(step4Ease).SetUpdate(useUnscaledTime));
if (cameraPivot != null)
sequence.Join(cameraPivot.DOLocalRotate(new Vector3(initialPivotPitch + step4Pitch, 0f, 0f), Mathf.Max(0.0001f, step4Duration), RotateMode.Fast).SetEase(step4Ease).SetUpdate(useUnscaledTime));
sequence.AppendCallback(FinishSequence);
}
#else
private System.Collections.IEnumerator PlayWithCoroutine()
{
yield return null;
}
#endif
private void FinishSequence()
{
Cursor.lockState = CursorLockMode.None;
Cursor.visible = true;
if (switchToUIActionMapAtEnd && PlayerControlLockService.Instance != null)
{
PlayerControlLockService.Instance.Lock(this);
}
if (endingUIPanel != null) endingUIPanel.SetActive(true);
}
private void PrepareOverlay(CanvasGroup cg, bool setAlpha, float alpha)
{
if (cg == null) { return; }
if (!cg.gameObject.activeSelf) cg.gameObject.SetActive(true);
if (cg.transform.parent != null && !cg.transform.parent.gameObject.activeSelf) cg.transform.parent.gameObject.SetActive(true);
var animator = cg.GetComponent<Animator>();
if (animator != null) animator.enabled = false;
var reachCanvasGroupAnimator = cg.GetComponent<Michsky.UI.Reach.CanvasGroupAnimator>();
if (reachCanvasGroupAnimator != null) reachCanvasGroupAnimator.enabled = false;
var reachImageFading = cg.GetComponent<Michsky.UI.Reach.ImageFading>();
if (reachImageFading != null) reachImageFading.enabled = false;
#if DOTWEEN
cg.DOKill();
#endif
cg.interactable = false;
cg.blocksRaycasts = false;
if (setAlpha) cg.alpha = alpha;
cg.transform.SetAsLastSibling();
Canvas canvas = cg.GetComponentInParent<Canvas>(true);
if (canvas != null)
{
canvas.overrideSorting = true;
canvas.sortingOrder = 5000;
}
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 837e4b6269a003744b0bfbddb6bed35e
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+8
View File
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 83931b34b2815d2428db8f4496b73a08
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
+8
View File
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 71b4a13048c30f549ac0172fd052eab4
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: fc04ef6c523954b4a94148d156261c5e
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,11 @@
using UnityEngine;
namespace Interaction.Conditions
{
public interface IInteractCondition
{
bool CanInteract(GameObject interactor, out string failReason);
void OnInteractSucceeded(GameObject interactor);
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 276a36eb0f3667446bb5302f67108f9f
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,11 @@
using UnityEngine;
namespace Interaction.Conditions
{
public abstract class InteractConditionBehaviour : MonoBehaviour, IInteractCondition
{
public abstract bool CanInteract(GameObject interactor, out string failReason);
public abstract void OnInteractSucceeded(GameObject interactor);
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 1d8be21aac1d2e84abce8f6482630586
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

Some files were not shown because too many files have changed in this diff Show More