diff --git a/posts/studycase3.md b/posts/studycase3.md
index b415847..737cd55 100644
--- a/posts/studycase3.md
+++ b/posts/studycase3.md
@@ -153,7 +153,7 @@ namespace StudyCase3
public float maxDistance = 5f;
public LayerMask layerMask;
public float itemsRotateSpeed = 120f;
- public Transform itemRoot;
+ [HideInInspector] public Transform itemRoot;
Item selectItem;
LineRenderer lineRenderer;
private void Awake()
diff --git a/posts/studycase4.md b/posts/studycase4.md
new file mode 100644
index 0000000..93c7e6a
--- /dev/null
+++ b/posts/studycase4.md
@@ -0,0 +1,289 @@
+---
+title: StudyCase3
+date: 2025-11-27 16:36:05
+tags: [studycase, unity]
+pinned: true
+head:
+ - - meta
+ - name: description
+ content: vitepress-theme-bluearchive StudyCase3
+ - - meta
+ - name: keywords
+ content: vitepress theme bluearchive StudyCase3
+---
+
+# 虚拟摇杆
+ - [世界构筑的起始篇章-编辑器](#_1-世界构筑的起始篇章-编辑器)
+ - [光芒汇聚之所-章节目标](#_2-光芒汇聚之所-章节目标)
+ - [光流影卷-演示视频](#_3-光流影卷-演示视频)
+ - [初始祭坛-前置准备](#_4-初始祭坛-前置准备)
+ - [勇者行迹录-章节任务](#_5-勇者行迹录-章节任务)
+ - [异闻录-FAQ](#_6-异闻录-faq)
+
+## 1.世界构筑的起始篇章-编辑器
+- **Unity 2022.3.62f2**:
+- **Visual Studio 2022**
+
+## 2.光芒汇聚之所-章节目标
+- **射线检测拾取物体**
+- **鼠标点击拾取物体**
+
+## 3.光流影卷-演示视频
+
+
+## 4.初始祭坛-前置准备
+### 导入[资源](/resources/studycase3/case3.zip)(天空球,描边,道具模型)
+
+## 5.勇者行迹录-章节任务
+### 创建StudyCase3
+- 复制场景和脚本改名为StudyCase3
+- 修改控制器的命名空间
+
+### 道具
+- 把导入的道具模型放到场景中
+
+
+
+- 新建道具层级
+
+
+
+- 在`Assets\Scripts\StudyCase3`下创建道具脚本`Item.cs`
+```csharp
+using UnityEngine;
+
+namespace StudyCase3
+{
+ public class Item : MonoBehaviour
+ {
+ Outline outline;
+ bool pick;
+ ThirdCharacterController player;
+ private void Awake()
+ {
+ player = FindAnyObjectByType();
+ if (outline == null)
+ outline = gameObject.AddComponent();
+ else
+ outline = GetComponent();
+ outline.enabled = false;
+ outline.OutlineMode = Outline.Mode.OutlineVisible;
+ outline.OutlineColor = new Color(0, 1, 1);
+ outline.OutlineWidth = 10f;
+ gameObject.layer = LayerMask.NameToLayer("Item");
+ }
+ public void Select()
+ {
+ if(!pick)
+ outline.enabled = true;
+ }
+ public void UnSelect()
+ {
+ if (!pick)
+ outline.enabled = false;
+ }
+ public void PickUp(Transform root)
+ {
+ if (pick) return;
+ pick = true;
+ transform.SetParent(root);
+ outline.OutlineWidth = 2f;
+ gameObject.layer = LayerMask.NameToLayer("Default");
+ player.SetItems();
+ }
+ private void OnMouseEnter()
+ {
+ if (player.pickMode == PickMode.Mouse)
+ Select();
+ }
+ private void OnMouseExit()
+ {
+ if (player.pickMode == PickMode.Mouse)
+ UnSelect();
+ }
+ private void OnMouseDown()
+ {
+ if (player.pickMode == PickMode.Mouse)
+ PickUp(player.itemRoot);
+ }
+ }
+}
+```
+- 道具挂载脚本
+
+
+
+- 创建Pick输入事件,绑定按键F
+
+
+
+- 修改`Assets\Scripts\StudyCase3` 下脚本 `ThirdCharacterController.cs`
+```csharp
+using UnityEngine;
+using UnityEngine.InputSystem;
+
+namespace StudyCase3
+{
+ public enum PickMode
+ {
+ RayCast,
+ Mouse
+ }
+ public class ThirdCharacterController : MonoBehaviour
+ {
+ [Header("MoveSettings")]
+ CharacterController characterController;
+ InputActionAsset inputAction;
+ Animator animator;
+ Transform forward;
+ Transform model;
+ Cinemachine.CinemachineVirtualCamera vCam;
+ public float moveSpeed = 5f;
+ public float jumpSpeed = 2f;
+ public float turnSpeed = 10f;
+ public float gravity = 10f;
+ Vector3 moveDir;
+ Vector2 moveInput;
+ [Header("PickSettings")]
+ public PickMode pickMode = PickMode.RayCast;
+ public CursorLockMode cursorLock;
+ public bool useDrawRay = true;
+ public float maxDistance = 5f;
+ public LayerMask layerMask;
+ public float itemsRotateSpeed = 120f;
+ public Transform itemRoot;
+ Item selectItem;
+ LineRenderer lineRenderer;
+ private void Awake()
+ {
+ Cursor.lockState = cursorLock;
+ characterController = GetComponent();
+ forward = transform.Find("Forward");
+ model = transform.Find("Model");
+ animator = model.GetComponentInChildren();
+ vCam = transform.Find("Virtual Camera").GetComponent();
+ inputAction = Resources.Load("PlayerInputActions");
+ inputAction.FindAction("Move").started += OnMove;
+ inputAction.FindAction("Move").performed += OnMove;
+ inputAction.FindAction("Move").canceled += OnMove;
+ inputAction.FindAction("Jump").performed += OnJump;
+ inputAction.FindAction("Pick").performed += OnPickUp;
+ inputAction.Enable();
+ itemRoot = transform.Find("ItemRoot");
+ if (lineRenderer == null)
+ {
+ lineRenderer = gameObject.AddComponent();
+ lineRenderer.material = new Material(Shader.Find("Universal Render Pipeline/2D/Sprite-Unlit-Default"));
+ lineRenderer.useWorldSpace = true;
+ }
+ }
+ private void Update()
+ {
+ Move();
+ CheckRayHit();
+ ItemsRotate();
+ }
+ public void OnMove(InputAction.CallbackContext context)
+ {
+ if (characterController.isGrounded)
+ moveInput = context.ReadValue();
+ else moveInput = Vector2.zero;
+ }
+ public void OnJump(InputAction.CallbackContext context)
+ {
+ if (context.performed && characterController.isGrounded)
+ {
+ moveDir.y = jumpSpeed;
+ }
+ }
+ void Move()
+ {
+ moveDir = new Vector3(moveInput.x, moveDir.y, moveInput.y);
+ forward.eulerAngles = new Vector3(0, vCam.transform.eulerAngles.y, 0);
+ moveDir = forward.TransformDirection(moveDir);
+ if (moveInput != Vector2.zero)
+ {
+ animator.SetBool("Move", true);
+ Quaternion target = Quaternion.LookRotation(new Vector3(moveDir.x, 0, moveDir.z));
+ model.rotation = Quaternion.Slerp(model.rotation, target, turnSpeed * Time.deltaTime);
+ }
+ else animator.SetBool("Move", false);
+ if (!characterController.isGrounded)
+ moveDir.y -= gravity * Time.deltaTime;
+ characterController.Move(moveDir * moveSpeed * Time.deltaTime);
+ }
+ #region PickUp
+ public void OnPickUp(InputAction.CallbackContext context)
+ {
+ if (pickMode != PickMode.RayCast) return;
+ if (selectItem != null)
+ selectItem.PickUp(itemRoot);
+ selectItem = null;
+ }
+ void CheckRayHit()
+ {
+ lineRenderer.enabled = useDrawRay;
+ if (pickMode != PickMode.RayCast)
+ {
+ useDrawRay = false;
+ return;
+ }
+ Ray ray = new Ray(model.position, model.forward);
+ RaycastHit hit;
+ if (Physics.Raycast(ray, out hit, maxDistance, layerMask))
+ {
+ DrawRay(model.position, hit.point, new Color(0, 1, 1));
+ Item curItem = hit.transform.GetComponent- ();
+ if (curItem != null && curItem != selectItem)
+ {
+ if (selectItem != null)
+ selectItem.UnSelect();
+ selectItem = curItem;
+ selectItem.Select();
+ }
+ }
+ else
+ {
+ DrawRay(model.position, model.position+ model.forward* maxDistance, new Color(1, 1, 1));
+ if (selectItem != null)
+ selectItem.UnSelect();
+ selectItem = null;
+ }
+ }
+ void DrawRay(Vector3 startPos, Vector3 endPos, Color color)
+ {
+ lineRenderer.startColor = lineRenderer.endColor = color;
+ lineRenderer.startWidth = lineRenderer.endWidth = 0.1f;
+ lineRenderer.positionCount = 2;
+ lineRenderer.SetPosition(0, startPos);
+ lineRenderer.SetPosition(1, endPos);
+ lineRenderer.enabled = true;
+ }
+ void ItemsRotate()
+ {
+ itemRoot.Rotate(0,itemsRotateSpeed*Time.deltaTime, 0);
+ }
+ public void SetItems()
+ {
+ float radius = 1.5f;
+ float angleStep = 360 / itemRoot.childCount;
+ for (int i = 0; i < itemRoot.childCount; i++)
+ {
+ Transform item = itemRoot.GetChild(i);
+ float angle = angleStep * i;
+ Quaternion rot = Quaternion.Euler(0, angle, 0);
+ item.localPosition = rot * itemRoot.forward * radius;
+ item.localScale = Vector3.one * 0.1f;
+ }
+ }
+ #endregion
+ }
+}
+```
+- 玩家挂载脚本,创建ItemRoot节点,设置射线检测层级
+
+
+
+## 6.异闻录-FAQ
\ No newline at end of file
diff --git a/public/resources/studycase4/mobile-controls-1.zip b/public/resources/studycase4/mobile-controls-1.zip
new file mode 100644
index 0000000..7c41f55
Binary files /dev/null and b/public/resources/studycase4/mobile-controls-1.zip differ