Initial Unity project commit
This commit is contained in:
@@ -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:
|
||||
@@ -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:
|
||||
@@ -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:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 83931b34b2815d2428db8f4496b73a08
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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
Reference in New Issue
Block a user