Land Explorer in HomeCamp with boot loading, journal UI, and CDN publish tooling.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,236 @@
|
||||
#if UNITY_EDITOR
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using UnityEditor;
|
||||
using UnityEditor.Animations;
|
||||
using UnityEngine;
|
||||
|
||||
public static class SetupExplorer
|
||||
{
|
||||
const string Root = "Assets/Bundles/Characters/Human/Explorer";
|
||||
const string FbxPath = Root + "/Explorer.fbx";
|
||||
const string IdleFbxPath = Root + "/Explorer@Idle.fbx";
|
||||
const string WalkFbxPath = Root + "/Explorer@Walk.fbx";
|
||||
const string BaseTexPath = Root + "/Explorer_base.png";
|
||||
const string MixTexPath = Root + "/Explorer_mix.png";
|
||||
const string MatPath = Root + "/Explorer.mat";
|
||||
const string ControllerPath = Root + "/Explorer.controller";
|
||||
const string PrefabPath = Root + "/Explorer.prefab";
|
||||
const float TargetHeight = 1.6f;
|
||||
|
||||
public static string Run()
|
||||
{
|
||||
AssetDatabase.Refresh(ImportAssetOptions.ForceSynchronousImport);
|
||||
SetupTexture(BaseTexPath, true);
|
||||
SetupTexture(MixTexPath, false);
|
||||
|
||||
var modelImporter = (ModelImporter)AssetImporter.GetAtPath(FbxPath);
|
||||
if (modelImporter == null) return "FAIL: ModelImporter not found for " + FbxPath;
|
||||
modelImporter.globalScale = 1.0f;
|
||||
modelImporter.useFileScale = false;
|
||||
modelImporter.bakeAxisConversion = true;
|
||||
modelImporter.materialImportMode = ModelImporterMaterialImportMode.None;
|
||||
modelImporter.animationType = ModelImporterAnimationType.Human;
|
||||
modelImporter.avatarSetup = ModelImporterAvatarSetup.CreateFromThisModel;
|
||||
modelImporter.SaveAndReimport();
|
||||
|
||||
var avatar = AssetDatabase.LoadAssetAtPath<Avatar>(FbxPath);
|
||||
Debug.Log($"[Explorer] Avatar generated: {avatar != null && avatar.isValid}");
|
||||
|
||||
SetupAnimationFbx(IdleFbxPath, avatar, "Idle");
|
||||
SetupAnimationFbx(WalkFbxPath, avatar, "Walk");
|
||||
|
||||
var mat = AssetDatabase.LoadAssetAtPath<Material>(MatPath);
|
||||
var shader = Shader.Find("Test/XPbr") ?? Shader.Find("Universal Render Pipeline/Lit");
|
||||
if (mat == null)
|
||||
{
|
||||
mat = new Material(shader) { name = "Explorer" };
|
||||
AssetDatabase.CreateAsset(mat, MatPath);
|
||||
}
|
||||
else
|
||||
{
|
||||
mat.shader = shader;
|
||||
}
|
||||
|
||||
var baseTex = AssetDatabase.LoadAssetAtPath<Texture2D>(BaseTexPath);
|
||||
var mixTex = AssetDatabase.LoadAssetAtPath<Texture2D>(MixTexPath);
|
||||
if (baseTex != null)
|
||||
{
|
||||
mat.SetTexture("_MainTex", baseTex);
|
||||
mat.SetTexture("_BaseMap", baseTex);
|
||||
}
|
||||
if (mixTex != null)
|
||||
{
|
||||
mat.SetTexture("_MixTex", mixTex);
|
||||
}
|
||||
mat.SetColor("_BaseColor", Color.white);
|
||||
mat.SetColor("_Color", Color.white);
|
||||
EditorUtility.SetDirty(mat);
|
||||
AssetDatabase.SaveAssetIfDirty(mat);
|
||||
|
||||
var controller = AssetDatabase.LoadAssetAtPath<AnimatorController>(ControllerPath);
|
||||
if (controller == null)
|
||||
{
|
||||
controller = AnimatorController.CreateAnimatorControllerAtPath(ControllerPath);
|
||||
}
|
||||
|
||||
if (controller.parameters.All(p => p.name != "isMoving"))
|
||||
controller.AddParameter("isMoving", AnimatorControllerParameterType.Bool);
|
||||
if (controller.parameters.All(p => p.name != "speed"))
|
||||
controller.AddParameter("speed", AnimatorControllerParameterType.Float);
|
||||
if (controller.parameters.All(p => p.name != "Motion"))
|
||||
controller.AddParameter("Motion", AnimatorControllerParameterType.Int);
|
||||
|
||||
var idleClip = LoadClip(IdleFbxPath);
|
||||
var walkClip = LoadClip(WalkFbxPath);
|
||||
|
||||
var rootStateMachine = controller.layers[0].stateMachine;
|
||||
foreach (var s in rootStateMachine.states.ToArray()) rootStateMachine.RemoveState(s.state);
|
||||
|
||||
var idleState = rootStateMachine.AddState("Idle");
|
||||
idleState.motion = idleClip;
|
||||
rootStateMachine.defaultState = idleState;
|
||||
|
||||
var walkState = rootStateMachine.AddState("Walk");
|
||||
walkState.motion = walkClip;
|
||||
|
||||
// Transition: Idle -> Walk
|
||||
var toWalk = idleState.AddTransition(walkState);
|
||||
toWalk.hasExitTime = false;
|
||||
toWalk.duration = 0.15f;
|
||||
toWalk.AddCondition(AnimatorConditionMode.If, 0, "isMoving");
|
||||
|
||||
// Transition: Walk -> Idle
|
||||
var toIdle = walkState.AddTransition(idleState);
|
||||
toIdle.hasExitTime = false;
|
||||
toIdle.duration = 0.15f;
|
||||
toIdle.AddCondition(AnimatorConditionMode.IfNot, 0, "isMoving");
|
||||
|
||||
// Also add Motion transitions for ExplorerLocomotion compatibility
|
||||
var toWalkMotion = idleState.AddTransition(walkState);
|
||||
toWalkMotion.hasExitTime = false;
|
||||
toWalkMotion.duration = 0.15f;
|
||||
toWalkMotion.AddCondition(AnimatorConditionMode.Equals, 1, "Motion");
|
||||
|
||||
var toIdleMotion = walkState.AddTransition(idleState);
|
||||
toIdleMotion.hasExitTime = false;
|
||||
toIdleMotion.duration = 0.15f;
|
||||
toIdleMotion.AddCondition(AnimatorConditionMode.Equals, 0, "Motion");
|
||||
|
||||
EditorUtility.SetDirty(controller);
|
||||
AssetDatabase.SaveAssetIfDirty(controller);
|
||||
|
||||
var fbxPrefab = AssetDatabase.LoadAssetAtPath<GameObject>(FbxPath);
|
||||
var instance = (GameObject)PrefabUtility.InstantiatePrefab(fbxPrefab);
|
||||
instance.name = "Explorer";
|
||||
|
||||
foreach (var r in instance.GetComponentsInChildren<SkinnedMeshRenderer>(true))
|
||||
{
|
||||
r.sharedMaterial = mat;
|
||||
}
|
||||
|
||||
var animator = instance.GetComponent<Animator>();
|
||||
if (animator == null) animator = instance.AddComponent<Animator>();
|
||||
animator.runtimeAnimatorController = controller;
|
||||
if (avatar != null) animator.avatar = avatar;
|
||||
animator.applyRootMotion = false;
|
||||
|
||||
// Add Locomotion Controller
|
||||
var ctrlType = AppDomain.CurrentDomain.GetAssemblies()
|
||||
.Select(a => a.GetType("HotUpdate.Gameplay.CampExplorerController"))
|
||||
.FirstOrDefault(t => t != null);
|
||||
if (ctrlType != null)
|
||||
{
|
||||
if (instance.GetComponent(ctrlType) == null) instance.AddComponent(ctrlType);
|
||||
}
|
||||
|
||||
// Add CapsuleCollider
|
||||
var col = instance.GetComponent<CapsuleCollider>();
|
||||
if (col == null) col = instance.AddComponent<CapsuleCollider>();
|
||||
col.center = new Vector3(0, 0.8f, 0);
|
||||
col.radius = 0.35f;
|
||||
col.height = 1.6f;
|
||||
|
||||
// Set layer 29
|
||||
foreach (var t in instance.GetComponentsInChildren<Transform>(true))
|
||||
{
|
||||
t.gameObject.layer = 29;
|
||||
}
|
||||
|
||||
var renderers = instance.GetComponentsInChildren<Renderer>(true);
|
||||
if (renderers.Length > 0)
|
||||
{
|
||||
var b = renderers[0].bounds;
|
||||
foreach (var r in renderers.Skip(1)) b.Encapsulate(r.bounds);
|
||||
float currentH = b.size.y;
|
||||
if (currentH > 0.01f)
|
||||
{
|
||||
float scale = TargetHeight / currentH;
|
||||
instance.transform.localScale = Vector3.one * scale;
|
||||
}
|
||||
}
|
||||
|
||||
PrefabUtility.SaveAsPrefabAsset(instance, PrefabPath);
|
||||
UnityEngine.Object.DestroyImmediate(instance);
|
||||
|
||||
AssetDatabase.SaveAssets();
|
||||
return $"SUCCESS: Authored {PrefabPath} with Avatar={avatar != null}, Idle={idleClip != null}, Walk={walkClip != null}";
|
||||
}
|
||||
|
||||
static void SetupTexture(string path, bool isSrgb)
|
||||
{
|
||||
var importer = (TextureImporter)AssetImporter.GetAtPath(path);
|
||||
if (importer == null) return;
|
||||
importer.textureType = TextureImporterType.Default;
|
||||
importer.sRGBTexture = isSrgb;
|
||||
importer.alphaIsTransparency = false;
|
||||
importer.mipmapEnabled = true;
|
||||
importer.maxTextureSize = 1024;
|
||||
importer.textureCompression = TextureImporterCompression.Compressed;
|
||||
importer.SaveAndReimport();
|
||||
}
|
||||
|
||||
static void SetupAnimationFbx(string path, Avatar sourceAvatar, string clipName)
|
||||
{
|
||||
var importer = (ModelImporter)AssetImporter.GetAtPath(path);
|
||||
if (importer == null) return;
|
||||
importer.globalScale = 1.0f;
|
||||
importer.importAnimation = true;
|
||||
importer.animationType = ModelImporterAnimationType.Human;
|
||||
if (sourceAvatar != null && sourceAvatar.isValid)
|
||||
{
|
||||
importer.avatarSetup = ModelImporterAvatarSetup.CopyFromOther;
|
||||
importer.sourceAvatar = sourceAvatar;
|
||||
}
|
||||
else
|
||||
{
|
||||
importer.avatarSetup = ModelImporterAvatarSetup.CreateFromThisModel;
|
||||
}
|
||||
|
||||
var clip = new ModelImporterClipAnimation
|
||||
{
|
||||
name = clipName,
|
||||
takeName = "Armature|Scene",
|
||||
loopTime = true,
|
||||
loopPose = true,
|
||||
lockRootRotation = true,
|
||||
lockRootHeightY = true,
|
||||
lockRootPositionXZ = true
|
||||
};
|
||||
importer.clipAnimations = new[] { clip };
|
||||
importer.SaveAndReimport();
|
||||
}
|
||||
|
||||
static AnimationClip LoadClip(string path)
|
||||
{
|
||||
var assets = AssetDatabase.LoadAllAssetsAtPath(path);
|
||||
foreach (var a in assets)
|
||||
{
|
||||
if (a is AnimationClip c && !c.name.StartsWith("__preview__"))
|
||||
return c;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
Reference in New Issue
Block a user