wire player control setup and tune the third-person controller

Editor/PlayerControlSetup.cs is the one-shot menu that assembles the playable
player: the armature, the Cinemachine rig, the NavMesh agent, and the pointer
input wiring, so a scene can be brought up to a controllable state repeatably
rather than by hand.

StarterAssets input actions gain the pointer and interaction bindings the new
locomotion and interaction layers read, and PlayerArmature is updated to match.

ThirdPersonController blends the animator toward target speed scaled by input
magnitude. Previously a partial stick deflection or a click-to-move arrival
played a slowed run clip via MotionSpeed alone; scaling the blend makes those
use the walk cycle instead.

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
2026-08-25 19:04:01 +02:00
co-authored by Claude Opus 5
parent cba87dcb31
commit ccabd3e7ac
5 changed files with 529 additions and 4 deletions
@@ -0,0 +1,316 @@
using NightclubArcadia.Cinematics;
using NightclubArcadia.Interaction;
using NightclubArcadia.Player;
using StarterAssets;
using Unity.AI.Navigation;
using Unity.Cinemachine;
using UnityEditor;
using UnityEditor.SceneManagement;
using UnityEngine;
using UnityEngine.AI;
/// <summary>
/// Wires player control, navigation, and camera systems into DialogueTest.unity.
/// Run via Tools → Nightclub Arcadia → Set Up Player Control &amp; Camera.
/// </summary>
public static class PlayerControlSetup
{
const string ScenePath = "Assets/Scenes/DialogueTest.unity";
const string PlayerPrefabPath = "Assets/StarterAssets/ThirdPersonController/Prefabs/PlayerArmature.prefab";
const string SettingsFolder = "Assets/Settings";
[MenuItem("Tools/Nightclub Arcadia/Set Up Player Control & Camera")]
public static void SetUp()
{
EnsurePlayerPrefab();
WireScene();
AssetDatabase.SaveAssets();
Debug.Log("[PlayerControlSetup] Player control and camera setup complete.");
}
static void EnsurePlayerPrefab()
{
var prefab = AssetDatabase.LoadAssetAtPath<GameObject>(PlayerPrefabPath);
if (prefab == null)
{
Debug.LogError($"[PlayerControlSetup] Missing prefab at {PlayerPrefabPath}");
return;
}
var root = PrefabUtility.LoadPrefabContents(PlayerPrefabPath);
EnsureComponent<PlayerPointerInput>(root);
EnsureComponent<PlayerMotionArbiter>(root);
EnsureComponent<ClickToMoveController>(root);
EnsureComponent<PointerNavigationInput>(root);
EnsureComponent<NavMeshAgent>(root);
var inputs = root.GetComponent<StarterAssetsInputs>();
if (inputs != null)
{
inputs.analogMovement = true;
inputs.cursorLocked = false;
}
var controller = root.GetComponent<ThirdPersonController>();
if (controller != null)
{
controller.LockCameraPosition = true;
}
var clickToMove = root.GetComponent<ClickToMoveController>();
var pointerNav = root.GetComponent<PointerNavigationInput>();
var pointerInput = root.GetComponent<PlayerPointerInput>();
var arbiter = root.GetComponent<PlayerMotionArbiter>();
WireSerializedReference(clickToMove, "brainCamera", null);
WireSerializedReference(pointerNav, "pointerInput", pointerInput);
WireSerializedReference(pointerNav, "clickToMove", clickToMove);
WireSerializedReference(pointerNav, "motionArbiter", arbiter);
PrefabUtility.SaveAsPrefabAsset(root, PlayerPrefabPath);
PrefabUtility.UnloadPrefabContents(root);
}
static void WireScene()
{
var scene = EditorSceneManager.OpenScene(ScenePath, OpenSceneMode.Single);
var player = GameObject.FindGameObjectWithTag("Player");
if (player == null)
{
Debug.LogError("[PlayerControlSetup] No Player-tagged object found in scene.");
return;
}
EnsureComponent<PlayerPointerInput>(player);
EnsureComponent<PlayerMotionArbiter>(player);
EnsureComponent<ClickToMoveController>(player);
EnsureComponent<PointerNavigationInput>(player);
EnsureComponent<NavMeshAgent>(player);
var inputs = player.GetComponent<StarterAssetsInputs>();
if (inputs != null)
{
inputs.analogMovement = true;
inputs.cursorLocked = false;
}
var controller = player.GetComponent<ThirdPersonController>();
if (controller != null)
{
controller.LockCameraPosition = true;
}
var brainCamera = Camera.main;
var pointerInput = player.GetComponent<PlayerPointerInput>();
var clickToMove = player.GetComponent<ClickToMoveController>();
var pointerNav = player.GetComponent<PointerNavigationInput>();
var arbiter = player.GetComponent<PlayerMotionArbiter>();
WireSerializedReference(clickToMove, "brainCamera", brainCamera);
WireSerializedReference(pointerNav, "brainCamera", brainCamera);
WireSerializedReference(pointerNav, "pointerInput", pointerInput);
WireSerializedReference(pointerNav, "clickToMove", clickToMove);
WireSerializedReference(pointerNav, "motionArbiter", arbiter);
var groundLayer = LayerMask.NameToLayer("Ground");
var interactableLayer = LayerMask.NameToLayer("Interactable");
SetLayerIfExists("Floor", groundLayer);
SetLayerIfExists("Room_101", groundLayer);
SetLayerIfExists("Wall_Back", groundLayer);
SetLayerIfExists("Wall_Front", groundLayer);
SetLayerIfExists("Table", groundLayer);
WireBartender("Bartender", interactableLayer);
WireBartender("Bartender (1)", interactableLayer);
EnsureNavigation(player.transform.position);
EnsureCameraAnchor(player.transform, brainCamera);
EditorSceneManager.MarkSceneDirty(scene);
EditorSceneManager.SaveScene(scene);
}
static void WireBartender(string objectName, int interactableLayer)
{
var bartender = GameObject.Find(objectName);
if (bartender == null)
{
return;
}
if (interactableLayer >= 0)
{
bartender.layer = interactableLayer;
}
EnsureComponent<ClickInteractableBridge>(bartender);
EnsureComponent<CameraPointOfInterest>(bartender);
}
static void EnsureNavigation(Vector3 sampleOrigin)
{
var navigation = GameObject.Find("Navigation");
if (navigation == null)
{
navigation = new GameObject("Navigation");
}
var surface = EnsureComponent<NavMeshSurface>(navigation);
surface.agentTypeID = 0;
surface.collectObjects = CollectObjects.All;
surface.useGeometry = NavMeshCollectGeometry.RenderMeshes;
var included = 0;
var defaultLayer = LayerMask.NameToLayer("Default");
var groundLayer = LayerMask.NameToLayer("Ground");
if (defaultLayer >= 0)
{
included |= 1 << defaultLayer;
}
if (groundLayer >= 0)
{
included |= 1 << groundLayer;
}
surface.layerMask = included;
var table = GameObject.Find("Table");
if (table != null)
{
var modifier = EnsureComponent<NavMeshModifier>(table);
modifier.overrideArea = true;
modifier.area = NavMesh.GetAreaFromName("Not Walkable");
}
surface.BuildNavMesh();
if (!NavMesh.SamplePosition(sampleOrigin, out _, 2f, NavMesh.AllAreas))
{
Debug.LogWarning("[PlayerControlSetup] NavMesh bake did not cover the player start position.");
}
}
static void EnsureCameraAnchor(Transform followTarget, Camera brainCamera)
{
var vcamObject = GameObject.Find("CinemachineCamera");
if (vcamObject == null)
{
Debug.LogError("[PlayerControlSetup] CinemachineCamera not found.");
return;
}
var vcam = vcamObject.GetComponent<CinemachineCamera>();
var composer = vcamObject.GetComponent<CinemachinePositionComposer>();
var anchorObject = GameObject.Find("CameraAnchor");
if (anchorObject == null)
{
anchorObject = new GameObject("CameraAnchor");
}
anchorObject.transform.position = followTarget.position;
var anchor = EnsureComponent<CameraRigAnchor>(anchorObject);
EnsureComponent<CameraPanInput>(anchorObject);
EnsureComponent<CameraPointOfInterestDirector>(anchorObject);
EnsureComponent<CameraStateDirector>(anchorObject);
WireSerializedReference(anchor, "followTarget", followTarget);
WireSerializedReference(anchor, "composer", composer);
if (vcam != null)
{
vcam.Target.TrackingTarget = anchorObject.transform;
vcam.Priority.Value = 10;
}
var pan = anchorObject.GetComponent<CameraPanInput>();
WireSerializedReference(pan, "brainCamera", brainCamera);
WireSerializedReference(pan, "anchor", anchor);
WireSerializedReference(pan, "stateDirector", anchorObject.GetComponent<CameraStateDirector>());
var poiDirector = anchorObject.GetComponent<CameraPointOfInterestDirector>();
WireSerializedReference(poiDirector, "anchor", anchor);
WireSerializedReference(poiDirector, "stateDirector", anchorObject.GetComponent<CameraStateDirector>());
var stateDirector = anchorObject.GetComponent<CameraStateDirector>();
WireSerializedReference(stateDirector, "anchor", anchor);
EnsureRevealCamera(vcamObject, followTarget);
}
static void EnsureRevealCamera(GameObject followVcam, Transform followTarget)
{
var revealName = "CM_Reveal_Room101";
var revealObject = GameObject.Find(revealName);
if (revealObject == null)
{
revealObject = new GameObject(revealName);
revealObject.transform.SetPositionAndRotation(
followVcam.transform.position + new Vector3(-4f, 2f, -2f),
followVcam.transform.rotation);
}
var revealCam = EnsureComponent<CinemachineCamera>(revealObject);
revealCam.Target.TrackingTarget = followTarget;
revealCam.Priority.Value = 0;
revealCam.Lens.FieldOfView = followVcam.GetComponent<CinemachineCamera>().Lens.FieldOfView;
var volumeObject = GameObject.Find("RevealVolume_Room101");
if (volumeObject == null)
{
volumeObject = new GameObject("RevealVolume_Room101");
volumeObject.transform.position = followTarget.position + new Vector3(0f, 1f, 2f);
}
var box = EnsureComponent<BoxCollider>(volumeObject);
box.isTrigger = true;
box.size = new Vector3(6f, 3f, 4f);
var volume = EnsureComponent<CameraFramingVolume>(volumeObject);
WireSerializedReference(volume, "revealCamera", revealCam);
}
static T EnsureComponent<T>(GameObject target) where T : Component
{
var component = target.GetComponent<T>();
if (component == null)
{
component = target.AddComponent<T>();
}
return component;
}
static void SetLayerIfExists(string objectName, int layer)
{
if (layer < 0)
{
return;
}
var obj = GameObject.Find(objectName);
if (obj != null)
{
obj.layer = layer;
}
}
static void WireSerializedReference(Object target, string propertyName, Object value)
{
if (target == null)
{
return;
}
var so = new SerializedObject(target);
var property = so.FindProperty(propertyName);
if (property != null)
{
property.objectReferenceValue = value;
so.ApplyModifiedPropertiesWithoutUndo();
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: eedfabcda58c94099b2ad3e19f141b09