|
用Unity自带的动画控制器Mechanim制作可视化状态机AI。
上码
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public abstract class LogicalStateMachineBehaviour : StateMachineBehaviour
{
// PRAGMA MARK - StateMachineBehaviour Lifecycle
public override void OnStateEnter(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
{
this.animator = animator;
this.stateInfo = stateInfo;
// You can reference other monobehaviours here
if (this.animator != null)
{
this.creature = animator.gameObject.GetComponent<Creature>();
}
this.OnStateEntered();
this._active = true;
}
public override void OnStateExit(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
{
this.animator = animator;
this.stateInfo = stateInfo;
this._active = false;
this.OnStateExited();
}
public override void OnStateUpdate(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
{
switch (animator.updateMode)
{
case AnimatorUpdateMode.AnimatePhysics:
this.deltaTime = Time.fixedDeltaTime;
break;
case AnimatorUpdateMode.Normal:
this.deltaTime = Time.deltaTime;
break;
case AnimatorUpdateMode.UnscaledTime:
this.deltaTime = Time.unscaledDeltaTime;
break;
default:
this.deltaTime = 0f;
break;
}
this.stateInfo = stateInfo;
this.OnStateUpdated();
}
// PRAGMA MARK - Internal
private bool _active = false;
protected Animator animator { get; private set; }
protected float deltaTime { get; private set; }
// TODO: You should store reference to the script that controls what this agent could do
// along with any other important information.
protected Creature creature { get; private set; }
protected AnimatorStateInfo stateInfo;
void OnDisable()
{
if (this._active)
{
this.OnStateExited();
}
}
// Implement the following functions to write actual state behaviors
protected virtual void OnStateEntered() { }
protected virtual void OnStateExited() { }
protected virtual void OnStateUpdated() { }
}这是一个中间脚本,你可以为每个状态创建一个StateMachineBehaviour脚本继承自它,override掉OnStateEntered、OnStateExited以及OnStateUpdated,就可以撰写每个状态具体的enter、update、以及exit的表现,而状态转移的逻辑则会放在Mechanim自身中,通过特定参数的条件判断来实现。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
/// <summary>
/// An Example Patrol behaviour of Mechanim State Machine AI.
/// At this state, the agent will get an random destination and try to move towards it.
/// This script only updates necessary parameters for the mechanim state transition to work.
/// The end condition of the state should be determined from the Mechanim State Machine editor.
/// </summary>
public class PatrolBehaviour : LogicalStateMachineBehaviour
{
public float StateDurationMin = 3.0f;
public float StateDurationMax = 6.0f;
public float newDestRadius = 10.0f;
public Vector3 destination { get; protected set; }
protected override void OnStateEntered()
{
this.animator.SetFloat(&#34;StateDuration&#34;, Random.Range(StateDurationMin, StateDurationMax));
GetRandomDestination();
}
protected override void OnStateUpdated()
{
// Update Parameters
this.animator.SetFloat(&#34;StateDuration&#34;, this.animator.GetFloat(&#34;StateDuration&#34;) - this.deltaTime);
this.animator.SetFloat(&#34;DistanceAway&#34;, Vector3.Distance(destination, creature.transform.position));
Debug.DrawLine(creature.transform.position, destination, Color.yellow);
creature.MoveTowards(destination, Time.fixedDeltaTime); // Replace with your movement function
}
protected override void OnStateExited()
{
this.animator.SetFloat(&#34;StateDuration&#34;, 0f);
}
void GetRandomDestination()
{
destination = creature.transform.position + Random.onUnitSphere * newDestRadius;
}
}我这里引用的Monobehaviour是Creature脚本,并使用MoveTowards方法来移动,你可以把它换为自己的。
然后可以在动画控制器中单独新开一个layer(也可以视情况使用原有的动画layer),编辑状态,把脚本挂到对应的状态上去,就可以用动画状态机的逻辑表现来控制AI。注意要把Transition duration设为0。
在这个状态机里,Creature会进入Patrol状态时生成一个倒计时StateDuration和一个目标点Destination,在倒计时过期或目标点距离过近时重新进入一次Patrol状态,从而重新生成游荡目标。
通过创建更多的状态表现,并控制状态转移的参数与条件,你可以轻松地使用极少的脚本和不使用外来插件的前提下定制自己的状态机表现,但是难以进行针对性的多线程性能优化,需要自己权衡利弊。
参见:
https://medium.com/the-unity-developers-handbook/dont-re-invent-finite-state-machines-how-to-repurpose-unity-s-animator-7c6c421e5785State Machine Behaviours - Unity
利用自定义Attribute轻松定制Property参数Inspector的GUI表现。
1、用MinMaxSlider调整上下界范围参数
示例:
[MinMaxSlider(0f, 1.0f)]
public Vector2 range = new Vector2(0.25f, 0.6f);
代码:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System;
[AttributeUsage(AttributeTargets.Field, AllowMultiple = true)]
public class MinMaxSliderAttribute : PropertyAttribute
{
public readonly float max;
public readonly float min;
//public readonly bool hasInt;
public MinMaxSliderAttribute(float min, float max)
{
this.min = min;
this.max = max;
//this.hasInt = hasInt;
}
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;
[CustomPropertyDrawer(typeof(MinMaxSliderAttribute))]
class MinMaxSliderDrawer : PropertyDrawer
{
//private const float FIELD_WIDTH = 40f;
private const float LABEL_WIDTH = 35f;
private const float FIELD_SPACE = 5f;
private const string LABEL_MIN = &#34;Min:&#34;;
private const string LABEL_MAX = &#34;Max:&#34;;
private int _numberOfLines = 2;
private Rect _rectPosition;
float _previousLabelWidth;
//float _previousFieldWidth;
MinMaxSliderAttribute minMaxSliderAttribute
{
get { return ((MinMaxSliderAttribute)attribute); }
}
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
{
_rectPosition = position;
if (property.propertyType != SerializedPropertyType.Vector2)
{
EditorGUI.LabelField(position, label, &#34;MinMaxSlider only works with Vector2&#34;);
return;
}
//EditorGUI.BeginProperty(position, label, property);
Vector2 range = property.vector2Value;
float min = range.x;
float max = range.y;
// Draw MinMax Slider
EditorGUI.BeginChangeCheck();
Rect rect = new Rect(position.x, position.y, position.width, GetHeight());
EditorGUI.MinMaxSlider(rect, label, ref min, ref max, minMaxSliderAttribute.min, minMaxSliderAttribute.max);
if (EditorGUI.EndChangeCheck())
{
range.x = min;
range.y = max;
property.vector2Value = range;
}
// Draw Float/Int Field
_previousLabelWidth = EditorGUIUtility.labelWidth;
//_previousFieldWidth = EditorGUIUtility.fieldWidth;
EditorGUIUtility.labelWidth = LABEL_WIDTH;
//EditorGUIUtility.fieldWidth = FIELD_WIDTH;
EditorGUI.BeginChangeCheck();
min = EditorGUI.FloatField(
new Rect(GetFieldX(0), GetFieldY(1), GetFieldWidth(), GetHeight()),
new GUIContent(LABEL_MIN), min);
max = EditorGUI.FloatField(
new Rect(GetFieldX(1), GetFieldY(1), GetFieldWidth(), GetHeight()),
new GUIContent(LABEL_MAX), max);
if (EditorGUI.EndChangeCheck())
{
range.x = Mathf.Clamp(min, minMaxSliderAttribute.min, minMaxSliderAttribute.max);
range.y = Mathf.Clamp(max, minMaxSliderAttribute.min, minMaxSliderAttribute.max);
property.vector2Value = range;
}
EditorGUIUtility.labelWidth = _previousLabelWidth;
//EditorGUIUtility.fieldWidth = _previousFieldWidth;
//EditorGUI.EndProperty();
}
public override float GetPropertyHeight(SerializedProperty property, GUIContent label) {
return _numberOfLines*(EditorGUI.GetPropertyHeight(property, label) + EditorGUIUtility.standardVerticalSpacing);
}
#region GUI_SIZE
float GetFieldWidth()
{
return (_rectPosition.width - _previousLabelWidth) / 2f - FIELD_SPACE;
}
float GetHeight()
{
return _rectPosition.height /(float)_numberOfLines - EditorGUIUtility.standardVerticalSpacing;
}
float GetFieldX(int id)
{
return _rectPosition.x + _previousLabelWidth + (GetFieldWidth() + FIELD_SPACE) * id;
}
float GetFieldY(int id)
{
return _rectPosition.y + GetHeight() * id;
}
#endregion
}(写的比较粗糙见谅)
这里用的是Vector2来作为上下界范围的数据类型(因为是存了两个浮点数),你可以根据实际需要修改,也可以自己定义一个struct。
适用场景主要是一些随机范围的选取,相当于把两个参数并为一个参数,并规定浮动范围,对Inspector的可视化有很大帮助。
参见:
https://gist.github.com/LotteMakesStuff/0de9be35044bab97cbe79b9ced695585
2、用布尔值bool/枚举类型enum决定某些参数在Inpector的显示情况。
篇幅问题代码丢在Github上了,参见以下链接中的ConditionalHide和EnumHide:
xylitogum/X-Utils
EnumHide范例
切换这里的MovementType会决定Inspector里是否显示对应类型下的特殊参数。
参考资料:
Hiding or Disabling inspector properties using PropertyDrawers within Unity 5 - Brecht Lecluyse
其他值得一提的Unity功能
ScriptableObject类,用作数据存放器将设置数据存在文件中,便于数据与逻辑的分离解耦,以及在不同Data或Config之间随时切换。
UnityEvent,类似Delegate,在Inspector中可视化的Event类,参见UGUI Button和Slider等事件编辑器,可以在编辑器中选择引用的外部物体、函数和对应参数,对减少代码量有极大帮助。在你需要的时机代码中Invoke即可。
AnimationCurve,可视化曲线编辑器,编辑取值0-1之间的贝塞尔曲线。本来是用作Unity动画编辑器的一部分,但你可以作为脚本参数类型,做出自己的参数放大器、自定义数学函数或其他动画效果等。用0-1之间的值作为x轴输入,调用Evaluate方法即可得到对应的y轴输出。Gradient同理。
ComputeShader,想要把通用运算放在GPU上?用它就对了。缺点是兼容性差和学习成本偏高,坑很深。StreamingAssets文件夹有人提到过了,在这个文件夹下的文件不会被打包进去而是独立存在的文件,方便外部引用和修改,也可以实现一些能够看到存档文件的保存功能。
|
本帖子中包含更多资源
您需要 登录 才可以下载或查看,没有账号?立即注册
×
|