Add prototype gameplay loop with character/battlefield assets and server game APIs.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,136 @@
|
||||
#if UNITY_EDITOR
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
using Object = UnityEngine.Object;
|
||||
|
||||
public static class VerifyAnimalWalk
|
||||
{
|
||||
const string Root = "Assets/Bundles/Characters/Animal/RockBall";
|
||||
public static Vector3[] WorldVertices(GameObject go)
|
||||
{
|
||||
return go.GetComponentsInChildren<SkinnedMeshRenderer>().SelectMany(s =>
|
||||
{
|
||||
var mesh = s.sharedMesh;
|
||||
var poses = mesh.bindposes;
|
||||
var matrices = s.bones.Select((b,i) => b.localToWorldMatrix * poses[i]).ToArray();
|
||||
var weights = mesh.boneWeights;
|
||||
return mesh.vertices.Select((v,i) =>
|
||||
{
|
||||
var w = weights[i];
|
||||
return matrices[w.boneIndex0].MultiplyPoint3x4(v) * w.weight0
|
||||
+ matrices[w.boneIndex1].MultiplyPoint3x4(v) * w.weight1
|
||||
+ matrices[w.boneIndex2].MultiplyPoint3x4(v) * w.weight2
|
||||
+ matrices[w.boneIndex3].MultiplyPoint3x4(v) * w.weight3;
|
||||
}).ToArray();
|
||||
}).ToArray();
|
||||
}
|
||||
public static Bounds GeometryBounds(GameObject go)
|
||||
{
|
||||
var points = WorldVertices(go);
|
||||
var result = new Bounds(points[0], Vector3.zero);
|
||||
foreach (var p in points) result.Encapsulate(p);
|
||||
return result;
|
||||
}
|
||||
public static string Inspect() => Check(false);
|
||||
public static string Run() => Check(true);
|
||||
public static string Live()
|
||||
{
|
||||
if(!EditorApplication.isPlaying) throw new Exception("Live check requires Play Mode");
|
||||
var unit=GameObject.Find("WalkVerified");
|
||||
if(unit==null) throw new Exception("Open RockBallWalkReview first");
|
||||
var animator=unit.GetComponentInChildren<Animator>();
|
||||
var state=animator.GetCurrentAnimatorStateInfo(0);
|
||||
var bounds=GeometryBounds(unit);
|
||||
if(!state.IsName("Walk") || animator.applyRootMotion || bounds.min.y < -.01f || bounds.min.y > .01f)
|
||||
throw new Exception($"Unexpected live gait: {state.shortNameHash} bottom={bounds.min.y}");
|
||||
return $"PASS Play Mode Walk time={state.normalizedTime:F3} bottom={bounds.min.y:F5} height={bounds.size.y:F5}";
|
||||
}
|
||||
static string Check(bool strict)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
var go = Object.Instantiate(AssetDatabase.LoadAssetAtPath<GameObject>(Root + "/RockBall_Rigged.prefab"));
|
||||
try
|
||||
{
|
||||
var anim = go.GetComponentInChildren<Animator>();
|
||||
anim.enabled = false;
|
||||
var model = anim.gameObject;
|
||||
var bones = model.GetComponentsInChildren<Transform>().Where(t => t.name.StartsWith("bone_")).ToArray();
|
||||
var bind = GeometryBounds(go);
|
||||
sb.AppendLine($"BIND height={bind.size.y:F5} minY={bind.min.y:F5} size={bind.size}");
|
||||
var restPositions = WorldVertices(go);
|
||||
var staticGo = Object.Instantiate(AssetDatabase.LoadAssetAtPath<GameObject>(Root + "/RockBall.prefab"));
|
||||
float shapeError;
|
||||
try
|
||||
{
|
||||
var points = staticGo.GetComponentsInChildren<MeshFilter>().SelectMany(f => f.sharedMesh.vertices.Select(v => f.transform.TransformPoint(v))).ToArray();
|
||||
shapeError = restPositions.Max(v => points.Min(p => Vector3.Distance(v,p)));
|
||||
}
|
||||
finally { Object.DestroyImmediate(staticGo); }
|
||||
sb.AppendLine($"BIND vs approved static maxVertexError={shapeError:F6}m bones={bones.Length}");
|
||||
var clip = AssetDatabase.LoadAssetAtPath<AnimationClip>(Root + "/Clips/Walk.anim");
|
||||
var root = bones.First(t => t.name == "bone_0");
|
||||
float minY = float.MaxValue, maxBottom = float.MinValue, tilt = 0, minHeight = float.MaxValue;
|
||||
float motion = 0, seam = 0;
|
||||
Vector3[] start = null;
|
||||
var rotations = new Quaternion[bones.Length];
|
||||
var rightFoot = bones.First(t => t.name == "bone_9");
|
||||
var leftFoot = bones.First(t => t.name == "bone_11");
|
||||
float minFootPhase = float.MaxValue, maxFootPhase = float.MinValue;
|
||||
var rightSamples = new Vector3[61];
|
||||
var leftSamples = new Vector3[61];
|
||||
for (int f = 0; f <= 60; f++)
|
||||
{
|
||||
clip.SampleAnimation(model, clip.length * f / 60f);
|
||||
var bounds = GeometryBounds(go);
|
||||
minY = Mathf.Min(minY, bounds.min.y);
|
||||
maxBottom = Mathf.Max(maxBottom, bounds.min.y);
|
||||
minHeight = Mathf.Min(minHeight, bounds.size.y);
|
||||
tilt = Mathf.Max(tilt, Vector3.Angle(root.up, Vector3.up));
|
||||
rightSamples[f] = rightFoot.position-root.position;
|
||||
leftSamples[f] = leftFoot.position-root.position;
|
||||
float phase = rightSamples[f].z-leftSamples[f].z;
|
||||
minFootPhase = Mathf.Min(minFootPhase,phase);
|
||||
maxFootPhase = Mathf.Max(maxFootPhase,phase);
|
||||
if (f == 0)
|
||||
{
|
||||
start = WorldVertices(go);
|
||||
for (int b = 0; b < bones.Length; b++) rotations[b] = bones[b].localRotation;
|
||||
}
|
||||
for (int b = 0; b < bones.Length; b++) motion = Mathf.Max(motion, Quaternion.Angle(rotations[b], bones[b].localRotation));
|
||||
if (f == 60)
|
||||
{
|
||||
var end = WorldVertices(go);
|
||||
seam = end.Select((v,i) => Vector3.Distance(v,start[i])).Max();
|
||||
}
|
||||
if (f % 15 == 0) sb.AppendLine($"Walk {f}/60 minY={bounds.min.y:F5} height={bounds.size.y:F5} rootTilt={Vector3.Angle(root.up,Vector3.up):F2}");
|
||||
}
|
||||
sb.AppendLine($"RESULT len={clip.length:F4} loop={AnimationUtility.GetAnimationClipSettings(clip).loopTime} minY={minY:F5} maxBottom={maxBottom:F5} minHeight={minHeight:F5} maxRootTilt={tilt:F2} maxJointMotion={motion:F2} seamVertexMax={seam:F6}");
|
||||
float rightStride = rightSamples.Max(p=>p.z)-rightSamples.Min(p=>p.z);
|
||||
float leftStride = leftSamples.Max(p=>p.z)-leftSamples.Min(p=>p.z);
|
||||
sb.AppendLine($"FEET rightStride={rightStride:F5}m leftStride={leftStride:F5}m relativePhaseRange={maxFootPhase-minFootPhase:F5}m");
|
||||
// Verify the real controller through several loops as well as clip sampling.
|
||||
anim.enabled = true;
|
||||
anim.Rebind();
|
||||
anim.SetInteger("Motion",1);
|
||||
anim.Play("Walk",0,0);
|
||||
anim.Update(0);
|
||||
for (int i = 0; i < 180; i++) anim.Update(1f/30f);
|
||||
var state = anim.GetCurrentAnimatorStateInfo(0);
|
||||
sb.AppendLine($"ANIMATOR stateWalk={state.IsName("Walk")} normalizedTime={state.normalizedTime:F2}");
|
||||
if (strict && (shapeError > .002f || Mathf.Abs(bind.size.y-.55f) > .002f || Mathf.Abs(bind.min.y) > .002f
|
||||
|| minY < -.01f || maxBottom > .025f || minHeight < .46f || tilt > 15f || motion < 5f || seam > .002f
|
||||
|| rightStride < .005f || leftStride < .005f || maxFootPhase-minFootPhase < .01f || !state.IsName("Walk")))
|
||||
throw new Exception(sb.ToString());
|
||||
var report = Path.GetFullPath(Path.Combine(Application.dataPath,"../../Tools/output/rockball_walk_audit/verification.txt"));
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(report));
|
||||
File.WriteAllText(report,sb.ToString());
|
||||
return (strict ? "PASS\n" : "INSPECT\n") + sb;
|
||||
}
|
||||
finally { Object.DestroyImmediate(go); }
|
||||
}
|
||||
}
|
||||
#endif
|
||||
Reference in New Issue
Block a user