ARTICLE AD BOX
Both the player and the wooden deck have colliders.
The player has the character controller component for the collider.
The wooden deck has a box collider.
This script is attached to the player:
using UnityEngine; [RequireComponent(typeof(CharacterController))] [RequireComponent(typeof(Animator))] public class ModernCharacterController : MonoBehaviour { [Header("Movement Settings")] public float walkSpeed = 4f; public float runSpeed = 8f; [Header("Vertical (Screen Up/Down) Movement")] public float verticalMoveSpeed = 3f; [Header("Jump Settings")] public float jumpHeight = 2f; public float gravity = -9.81f; [Header("Animation Smoothing")] [Tooltip("How fast the Speed parameter ramps up (higher = snappier)")] public float speedSmoothRate = 10f; [Header("2.5D Settings")] public bool lockZAxis = true; private CharacterController controller; private Animator animator; private float lockedZPosition; private Vector3 velocity; private bool isGrounded; private float currentAnimSpeed; // smoothed value sent to animator void Start() { controller = GetComponent<CharacterController>(); animator = GetComponent<Animator>(); lockedZPosition = transform.position.z; // Face right (positive X) for side-scroller transform.rotation = Quaternion.Euler(0, 90, 0); // Auto-place player on ground SnapToGround(); } void SnapToGround() { // Temporarily disable the CharacterController so we can set position directly controller.enabled = false; RaycastHit hit; Vector3 rayOrigin = transform.position + Vector3.up * 10f; if (Physics.Raycast(rayOrigin, Vector3.down, out hit, 50f)) { transform.position = new Vector3(transform.position.x, hit.point.y, transform.position.z); Debug.Log($"Snapped to ground at Y: {hit.point.y} (hit: {hit.collider.name})"); } controller.enabled = true; } void Update() { HandleMovement(); HandleAnimator(); } void HandleMovement() { // Ground check isGrounded = controller.isGrounded; if (isGrounded && velocity.y < 0) { velocity.y = -2f; } // Raw input for instant response float moveXRaw = Input.GetAxisRaw("Horizontal"); float moveYRaw = Input.GetAxisRaw("Vertical"); // Determine if running bool isRunning = Input.GetKey(KeyCode.LeftShift) || Input.GetKey(KeyCode.RightShift); float currentMoveSpeed = isRunning ? runSpeed : walkSpeed; // Build movement vector Vector3 move = new Vector3(moveXRaw * currentMoveSpeed, 0f, 0f); // Optional vertical screen movement (W/S) Vector3 verticalMove = new Vector3(0f, moveYRaw * verticalMoveSpeed, 0f); // Face horizontal direction if (moveXRaw > 0.1f) transform.rotation = Quaternion.Euler(0, 90, 0); else if (moveXRaw < -0.1f) transform.rotation = Quaternion.Euler(0, -90, 0); // Apply horizontal + vertical movement controller.Move((move + verticalMove) * Time.deltaTime); // Jump if (Input.GetButtonDown("Jump") && isGrounded) { velocity.y = Mathf.Sqrt(jumpHeight * -2f * gravity); animator.SetTrigger("Jump"); } // Apply gravity velocity.y += gravity * Time.deltaTime; controller.Move(velocity * Time.deltaTime); // Lock Z axis if (lockZAxis) { float zDrift = transform.position.z - lockedZPosition; if (Mathf.Abs(zDrift) > 0.001f) { controller.Move(new Vector3(0f, 0f, -zDrift)); } } } void HandleAnimator() { float moveXRaw = Mathf.Abs(Input.GetAxisRaw("Horizontal")); bool isRunning = Input.GetKey(KeyCode.LeftShift) || Input.GetKey(KeyCode.RightShift); // Target speed for blend tree: 0 = idle, 0.5 = walk, 1 = run float targetAnimSpeed = 0f; if (moveXRaw > 0.1f) { targetAnimSpeed = isRunning ? 1f : 0.5f; } // Smooth it so animation transitions look natural currentAnimSpeed = Mathf.MoveTowards(currentAnimSpeed, targetAnimSpeed, speedSmoothRate * Time.deltaTime); animator.SetFloat("Speed", currentAnimSpeed); animator.SetBool("IsGrounded", isGrounded); } }And this script is attached to the empty gameobject DockManager:
using System.Collections.Generic; using UnityEngine; public class DockManager : MonoBehaviour { public GameObject dockPrefab; public int sideViewSegments = 6; public int thirdPersonSegments = 24; public Transform player; [Header("Segment Settings")] [Tooltip("Leave 0 to auto-calculate from prefab")] public float segmentLengthOverride = 0f; [Header("Auto-calculated")] [SerializeField] private float segmentLength; private List<GameObject> segments = new List<GameObject>(); private float lastSpawnX; private int currentSegmentCount; void Start() { if (segmentLengthOverride > 0f) { segmentLength = segmentLengthOverride; } else { Renderer rend = dockPrefab.GetComponentInChildren<Renderer>(); if (rend != null) { GameObject temp = Instantiate(dockPrefab, Vector3.zero, dockPrefab.transform.rotation); Renderer[] allRenderers = temp.GetComponentsInChildren<Renderer>(); Bounds combinedBounds = allRenderers[0].bounds; for (int i = 1; i < allRenderers.Length; i++) { combinedBounds.Encapsulate(allRenderers[i].bounds); } segmentLength = combinedBounds.size.x; Debug.Log($"Calculated segment length: {segmentLength}"); Destroy(temp); } } currentSegmentCount = sideViewSegments; lastSpawnX = 0f; for (int i = 0; i < currentSegmentCount; i++) { SpawnSegment(false); } } void Update() { if (segments.Count == 0) return; if (player.position.x > segments[0].transform.position.x + segmentLength) { RecycleSegment(); } } // Call this from SimpleCameraFollow when switching views public void SetViewMode(bool isThirdPerson) { int targetCount = isThirdPerson ? thirdPersonSegments : sideViewSegments; if (targetCount > currentSegmentCount) { // Add more segments ahead int toAdd = targetCount - currentSegmentCount; for (int i = 0; i < toAdd; i++) { SpawnSegment(true); } } else if (targetCount < currentSegmentCount) { // Remove excess segments from the back (farthest ahead) int toRemove = currentSegmentCount - targetCount; for (int i = 0; i < toRemove; i++) { if (segments.Count > targetCount) { GameObject last = segments[segments.Count - 1]; segments.RemoveAt(segments.Count - 1); Destroy(last); lastSpawnX -= segmentLength; } } } currentSegmentCount = targetCount; } void SpawnSegment(bool assignColor) { GameObject segment = Instantiate( dockPrefab, new Vector3(lastSpawnX, 0, 0), dockPrefab.transform.rotation ); segments.Add(segment); if (assignColor) SetRandomColor(segment); lastSpawnX += segmentLength; } void RecycleSegment() { GameObject first = segments[0]; segments.RemoveAt(0); first.transform.position = new Vector3(lastSpawnX, 0, 0); SetRandomColor(first); segments.Add(first); lastSpawnX += segmentLength; } void SetRandomColor(GameObject segment) { Renderer renderer = segment.GetComponent<Renderer>(); if (renderer == null) return; MaterialPropertyBlock block = new MaterialPropertyBlock(); renderer.GetPropertyBlock(block); Color randomColor = Random.ColorHSV(0f, 1f, 0.6f, 1f, 0.6f, 1f); block.SetColor("_BaseColor", randomColor); block.SetColor("_Color", randomColor); renderer.SetPropertyBlock(block); } }



