Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,24 @@

[English](CHANGELOG_EN.md) | 中文

## v1.8.6

- 新增 `ModelParameterDriver` 组件,支持在动画状态机中自定义参数控制
- 支持多种参数操作类型:
- `Set`:直接设置参数值
- `Add`:在现有值基础上增加指定值
- `Random`:随机设置参数值(支持范围随机和概率触发)
- `Copy`:从源参数复制值到目标参数(支持范围转换)
- 支持所有 Animator 参数类型(Float、Int、Bool、Trigger)
- 在动画状态进入时自动应用参数驱动
- 支持参数验证,确保目标参数和源参数存在后才应用驱动
- 新增 `AnimatorParameterDriverManager` 管理器,统一管理参数驱动的初始化和应用逻辑
- 增强动画参数显示器功能:
- 添加参数缓存机制,优化参数获取性能,减少重复计算
- 支持显示 Animator 控制器中定义的外部参数,默认排列在列表末尾
- 新增 `BlueprintID` 组件,用于为游戏对象分配唯一标识符(当前暂无实际功能)
- 更新依赖版本:`DuckovGameLibs` 从 1.1.6-Steam 更新到 1.2.5-Steam

## v1.8.5

- 改进音效播放系统,统一使用 `ModelHandler.PlaySound` 方法管理所有音效播放
Expand Down
18 changes: 18 additions & 0 deletions CHANGELOG_EN.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,24 @@

English | [中文](CHANGELOG.md)

## v1.8.6

- Added `ModelParameterDriver` component, supporting custom parameter control in animation state machines
- Supports multiple parameter operation types:
- `Set`: Directly set parameter value
- `Add`: Add specified value to existing value
- `Random`: Randomly set parameter value (supports range randomization and probability triggering)
- `Copy`: Copy value from source parameter to target parameter (supports range conversion)
- Supports all Animator parameter types (Float, Int, Bool, Trigger)
- Automatically applies parameter driver when animation state enters
- Supports parameter validation, ensuring target and source parameters exist before applying driver
- Added `AnimatorParameterDriverManager` manager to uniformly manage parameter driver initialization and application logic
- Enhanced animator parameter display functionality:
- Added parameter caching mechanism to optimize parameter retrieval performance and reduce redundant calculations
- Supports displaying external parameters defined in Animator controller, defaulting to the end of the list
- Added `BlueprintID` component for assigning unique identifiers to game objects (currently no actual functionality)
- Updated dependency version: `DuckovGameLibs` updated from 1.1.6-Steam to 1.2.5-Steam

## v1.8.5

- Improved audio playback system, unified use of `ModelHandler.PlaySound` method to manage all audio playback
Expand Down
1 change: 1 addition & 0 deletions DuckovCustomModel.Core/Data/CustomAnimatorHash.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ public class AnimatorParamInfo
public int Hash { get; set; }
public string Type { get; set; } = string.Empty;
public object? InitialValue { get; set; }
public bool IsExternal { get; set; }
}

public static class CustomAnimatorHash
Expand Down
1 change: 0 additions & 1 deletion DuckovCustomModel.Core/Data/SoundPlayMode.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,3 @@ public enum SoundPlayMode
UseTempObject,
}
}

2 changes: 1 addition & 1 deletion DuckovCustomModel.Core/DuckovCustomModel.Core.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@

<!-- CI 环境使用 NuGet 包 -->
<ItemGroup Condition="'$(UseGameLibsFromNuGet)' == 'true'">
<PackageReference Include="DuckovGameLibs" Version="1.1.6-Steam"/>
<PackageReference Include="DuckovGameLibs" Version="1.2.5-Steam"/>
</ItemGroup>

<!-- 本地开发使用游戏目录 -->
Expand Down
194 changes: 194 additions & 0 deletions DuckovCustomModel.Core/Managers/AnimatorParameterDriverManager.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
using System.Linq;
using DuckovCustomModel.Core.MonoBehaviours.Animators;
using UnityEngine;

namespace DuckovCustomModel.Core.Managers
{
public static class AnimatorParameterDriverManager
{
public static void InitializeDriver(ModelParameterDriver parameterDriver, Animator animator)
{
if (parameterDriver.Initialized) return;

parameterDriver.Initialized = true;

var enableParameters = parameterDriver.parameters
.Where(parameter => InitializeParameter(parameter, animator)).ToArray();

parameterDriver.parameters = enableParameters;
parameterDriver.IsEnabled = parameterDriver.parameters.Length > 0;
}

public static void ApplyParameter(ModelParameterDriver.Parameter parameter, Animator animator)
{
switch (parameter.type)
{
case ModelParameterDriver.ChangeType.Set:
{
ApplyParameterAsSet(parameter, animator);
break;
}
case ModelParameterDriver.ChangeType.Add:
{
ApplyParameterAsAdd(parameter, animator);
break;
}
case ModelParameterDriver.ChangeType.Random:
{
ApplyParameterAsRandom(parameter, animator);
break;
}
case ModelParameterDriver.ChangeType.Copy:
{
ApplyParameterAsCopy(parameter, animator);
break;
}
}
}

private static bool InitializeParameter(ModelParameterDriver.Parameter parameter, Animator animator)
{
if (string.IsNullOrEmpty(parameter.name)) return false;

var parameterExists = animator.parameters
.Any(p => p.name == parameter.name);

if (!parameterExists) return false;

var destParam = animator.parameters
.FirstOrDefault(p => p.name == parameter.name);
if (destParam == null) return false;

parameter.DestParam = destParam;

if (parameter.type != ModelParameterDriver.ChangeType.Copy) return true;
{
if (string.IsNullOrEmpty(parameter.source)) return false;

var sourceParam = animator.parameters
.FirstOrDefault(p => p.name == parameter.source);
if (sourceParam == null) return false;

parameter.SourceParam = sourceParam;
}

return true;
}

private static void ApplyParameterAsSet(ModelParameterDriver.Parameter parameter, Animator animator)
{
if (parameter.DestParam is not AnimatorControllerParameter targetParam)
return;

switch (targetParam.type)
{
case AnimatorControllerParameterType.Float:
animator.SetFloat(targetParam.name, parameter.value);
break;
case AnimatorControllerParameterType.Int:
animator.SetInteger(targetParam.name, (int)parameter.value);
break;
case AnimatorControllerParameterType.Bool:
animator.SetBool(targetParam.name, parameter.value > 0f);
break;
case AnimatorControllerParameterType.Trigger:
animator.SetTrigger(targetParam.name);
break;
}
}

private static void ApplyParameterAsAdd(ModelParameterDriver.Parameter parameter, Animator animator)
{
if (parameter.DestParam is not AnimatorControllerParameter targetParam)
return;

switch (targetParam.type)
{
case AnimatorControllerParameterType.Float:
{
var currentValue = animator.GetFloat(targetParam.name);
animator.SetFloat(targetParam.name, currentValue + parameter.value);
break;
}
case AnimatorControllerParameterType.Int:
{
var currentValue = animator.GetInteger(targetParam.name);
animator.SetInteger(targetParam.name, currentValue + (int)parameter.value);
break;
}
}
}

private static void ApplyParameterAsRandom(ModelParameterDriver.Parameter parameter, Animator animator)
{
if (parameter.DestParam is not AnimatorControllerParameter targetParam)
return;

switch (targetParam.type)
{
case AnimatorControllerParameterType.Float:
var randomFloat = Random.Range(parameter.valueMin, parameter.valueMax);
animator.SetFloat(targetParam.name, randomFloat);
break;
case AnimatorControllerParameterType.Int:
var randomInt = Random.Range((int)parameter.valueMin, (int)parameter.valueMax);

Copilot AI Nov 21, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unity's Random.Range for integers is exclusive on the upper bound, meaning Random.Range((int)parameter.valueMin, (int)parameter.valueMax) will never return valueMax. This differs from the float version which is inclusive. Consider documenting this behavior or adding 1 to valueMax if inclusive behavior is intended: Random.Range((int)parameter.valueMin, (int)parameter.valueMax + 1).

Suggested change
var randomInt = Random.Range((int)parameter.valueMin, (int)parameter.valueMax);
var randomInt = Random.Range((int)parameter.valueMin, (int)parameter.valueMax + 1);

Copilot uses AI. Check for mistakes.
animator.SetInteger(targetParam.name, randomInt);
break;
case AnimatorControllerParameterType.Bool:
var randomBool = Random.value < parameter.chance;
animator.SetBool(targetParam.name, randomBool);
break;
case AnimatorControllerParameterType.Trigger:
if (Random.value < parameter.chance)
animator.SetTrigger(targetParam.name);
break;
}
}

private static void ApplyParameterAsCopy(ModelParameterDriver.Parameter parameter, Animator animator)
{
if (parameter.DestParam is not AnimatorControllerParameter targetParam)
return;

if (parameter.SourceParam is not AnimatorControllerParameter sourceParam)
return;

if (sourceParam.type == AnimatorControllerParameterType.Trigger)
return;

var sourceValue = sourceParam.type switch
{
AnimatorControllerParameterType.Float => animator.GetFloat(sourceParam.name),
AnimatorControllerParameterType.Int => animator.GetInteger(sourceParam.name),
AnimatorControllerParameterType.Bool => animator.GetBool(sourceParam.name) ? 1f : 0f,
_ => 0f,
};

var finalValue = sourceValue;
if (parameter.convertRange)
{
var sourceMin = parameter.sourceMin;
var sourceMax = parameter.sourceMax;
var targetMin = parameter.valueMin;
var targetMax = parameter.valueMax;

if (sourceMax - sourceMin != 0)

Copilot AI Nov 21, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Equality checks on floating point values can yield unexpected results.

Suggested change
if (sourceMax - sourceMin != 0)
if (Mathf.Abs(sourceMax - sourceMin) > Mathf.Epsilon)

Copilot uses AI. Check for mistakes.
finalValue = targetMin + (sourceValue - sourceMin) * (targetMax - targetMin) /
(sourceMax - sourceMin);
}

switch (targetParam.type)
{
case AnimatorControllerParameterType.Float:
animator.SetFloat(targetParam.name, finalValue);
break;
case AnimatorControllerParameterType.Int:
animator.SetInteger(targetParam.name, (int)finalValue);
break;
case AnimatorControllerParameterType.Bool:
animator.SetBool(targetParam.name, finalValue > 0f);
break;
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
using System;
using DuckovCustomModel.Core.Managers;
using Newtonsoft.Json;
using UnityEngine;

namespace DuckovCustomModel.Core.MonoBehaviours.Animators
{
public class ModelParameterDriver : StateMachineBehaviour, ISerializationCallbackReceiver
{
Comment on lines +8 to +9

Copilot AI Nov 21, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The class lacks documentation explaining its purpose and usage. Consider adding a summary comment explaining that this component drives animator parameters when animation states are entered, similar to Unity's built-in Animator Parameter Driver behavior. This would help users understand how to use it in their animation state machines.

Suggested change
public class ModelParameterDriver : StateMachineBehaviour, ISerializationCallbackReceiver
{
/// <summary>
/// Drives animator parameters when animation states are entered, similar to Unity's built-in Animator Parameter Driver.
/// Attach this component to animation state machine states to automatically set, add, randomize, or copy animator parameters
/// when the state is entered. Useful for controlling complex animation logic and parameter changes in a modular way.
/// </summary>
public class ModelParameterDriver : StateMachineBehaviour, ISerializationCallbackReceiver

Copilot uses AI. Check for mistakes.
public enum ChangeType
{
Set,
Add,
Random,
Copy,
}

public Parameter[] parameters = [];

[HideInInspector] [SerializeField] private string parametersData = string.Empty;

[Tooltip(
"Custom debug message that will be written to the client logs when the ParameterDriver is used. Be careful to remove these before your final upload as this can spam your log files.")]
public string debugString = string.Empty;

[NonSerialized] public bool Initialized;

[NonSerialized] public bool IsEnabled;

public override void OnStateEnter(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
{
if (!Initialized)
AnimatorParameterDriverManager.InitializeDriver(this, animator);

if (!IsEnabled)
return;

if (!string.IsNullOrEmpty(debugString))
ModLogger.Log($"[AnimatorParameterDriverManager] ParameterDriver Debug: {debugString}");

foreach (var parameter in parameters) AnimatorParameterDriverManager.ApplyParameter(parameter, animator);
}

public void OnBeforeSerialize()
{
parametersData = JsonConvert.SerializeObject(parameters);
}

public void OnAfterDeserialize()
{
if (!string.IsNullOrEmpty(parametersData))
parameters = JsonConvert.DeserializeObject<Parameter[]>(parametersData) ?? [];
}

[Serializable]
public class Parameter
{
[Tooltip("The type of operation to be executed")]
public ChangeType type;

[Tooltip("Parameter that will be written to")]
public string name = string.Empty;

[Tooltip("Source parameter that will be read")]
public string source = string.Empty;

[Tooltip("The value used for this operation")]
public float value;

[Tooltip("Minimum value to be set")] public float valueMin;
[Tooltip("Maximum value to be set")] public float valueMax = 1f;

[Tooltip(
"Chance the value will be set. When used with a Bool type, defines the chance the value is set to 1, otherwise it's set to 0.")]
[Range(0.0f, 1f)]
public float chance = 1f;

[Tooltip(
"If true, we convert the range of the source and destination values according to the ranges given.")]
public bool convertRange;

public float sourceMin;
public float sourceMax;
public float destMin;
public float destMax;
Comment on lines +84 to +85

Copilot AI Nov 21, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The fields destMin and destMax are defined but never used. The range conversion logic in AnimatorParameterDriverManager.ApplyParameterAsCopy (lines 172-173) uses valueMin and valueMax instead. Either remove these unused fields or update the range conversion logic to use them if they were intended for a different purpose than valueMin/valueMax.

Suggested change
public float destMin;
public float destMax;

Copilot uses AI. Check for mistakes.

public object? DestParam;
Comment on lines +86 to +87

Copilot AI Nov 21, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] Public properties DestParam and SourceParam store cached parameter references but lack documentation explaining their purpose and that they are populated during initialization. Consider adding XML documentation comments to clarify that these are runtime-only cached values used for performance optimization.

Suggested change
public object? DestParam;
/// <summary>
/// Runtime-only cached reference to the destination parameter.
/// Populated during initialization for performance optimization.
/// Not intended to be set manually or serialized.
/// </summary>
public object? DestParam;
/// <summary>
/// Runtime-only cached reference to the source parameter.
/// Populated during initialization for performance optimization.
/// Not intended to be set manually or serialized.
/// </summary>

Copilot uses AI. Check for mistakes.
public object? SourceParam;
}
}
}
18 changes: 18 additions & 0 deletions DuckovCustomModel.Core/MonoBehaviours/Packages/BlueprintID.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
using System;
using UnityEngine;

namespace DuckovCustomModel.Core.MonoBehaviours.Packages
{
[AddComponentMenu("Duckov Custom Model/Blueprint ID")]
[DisallowMultipleComponent]

Copilot AI Nov 21, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The class lacks documentation explaining its purpose. According to the PR description, it "assigns unique identifiers to game objects" but currently has no functionality. Consider adding a summary comment explaining its intended purpose and that it's currently a placeholder for future functionality.

Suggested change
[DisallowMultipleComponent]
[DisallowMultipleComponent]
/// <summary>
/// Assigns unique identifiers to game objects. Currently a placeholder for future functionality.
/// </summary>

Copilot uses AI. Check for mistakes.
public class BlueprintID : MonoBehaviour
{

Copilot AI Nov 21, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The public field id should have a tooltip attribute to explain its purpose in the Unity Inspector. Consider adding [Tooltip("Unique identifier for this game object")] above the field declaration to improve usability for users configuring this component in Unity.

Suggested change
{
{
[Tooltip("Unique identifier for this game object")]

Copilot uses AI. Check for mistakes.
public string id = string.Empty;

[ContextMenu("Generate New ID")]
private void GenerateNewID()
{
id = Guid.NewGuid().ToString();
}
}
}
2 changes: 1 addition & 1 deletion DuckovCustomModel/Constant.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ public static class Constant
{
public const string ModID = "DuckovCustomModel";
public const string ModName = "Duckov Custom Model";
public const string ModVersion = "1.8.5";
public const string ModVersion = "1.8.6";
public const string HarmonyId = "com.ritsukage.DuckovCustomModel";
}
}
2 changes: 1 addition & 1 deletion DuckovCustomModel/DuckovCustomModel.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@

<!-- CI 环境使用 NuGet 包 -->
<ItemGroup Condition="'$(UseGameLibsFromNuGet)' == 'true'">
<PackageReference Include="DuckovGameLibs" Version="1.1.6-Steam"/>
<PackageReference Include="DuckovGameLibs" Version="1.2.5-Steam"/>
</ItemGroup>

<!-- 本地开发使用游戏目录 -->
Expand Down
Loading
Loading