前言
欢迎来到【制作100个Unity游戏】系列!本系列将引导您一步步学习如何使用Unity开发各种类型的游戏。在这第25篇中,我们将探索如何用unity制作一个3D背包、库存、制作、快捷栏、存储系统、砍伐树木获取资源、随机战利品宝箱等功能,我会附带项目源码,以便你更好理解它。
人物和视角基本控制
具体可以看我这篇文章:
【unity小技巧】unity最完美的CharacterController 3d角色控制器,实现移动、跳跃、下蹲、奔跑、上下坡、物理碰撞效果,复制粘贴即用
这里我就直接贴出代码了
人物移动控制
[RequireComponent(typeof(CharacterController))] public class MovementScript : MonoBehaviour { [Tooltip("角色控制器")] public CharacterController characterController; [Tooltip("重力加速度")] private float Gravity = -19.8f; private float horizontal; private float vertical; [Header("移动")] [Tooltip("角色行走的速度")] public float walkSpeed = 6f; [Tooltip("角色奔跑的速度")] public float runSpeed = 9f; [Tooltip("角色移动的方向")] private Vector3 moveDirection; [Tooltip("当前速度")] private float speed; [Tooltip("是否奔跑")] private bool isRun; [Header("地面检测")] [Tooltip("是否在地面")] private bool isGround; [Header("跳跃")] [Tooltip("角色跳跃的高度")] public float jumpHeight = 8f; private float _verticalVelocity; void Start() { speed = walkSpeed; } void Update() { horizontal = Input.GetAxis("Horizontal"); vertical = Input.GetAxis("Vertical"); //地面检测 isGround = characterController.isGrounded; SetSpeed(); SetRun(); SetMove(); SetJump(); } //速度设置 void SetSpeed() { if (isRun) { speed = runSpeed; } else { speed = walkSpeed; } } //控制奔跑 void SetRun() { if (Input.GetKey(KeyCode.LeftShift)) { isRun = true; } else { isRun = false; } } //控制移动 void SetMove() { moveDirection = transform.right * horizontal + transform.forward * vertical; // 计算移动方向 moveDirection = moveDirection.normalized; // 归一化移动方向,避免斜向移动速度过快 } //控制跳跃 void SetJump() { bool jump = Input.GetButtonDown("Jump"); if (isGround) { // 在着地时阻止垂直速度无限下降 if (_verticalVelocity < 0.0f) { _verticalVelocity = -2f; } if (jump) { _verticalVelocity = jumpHeight; } } else { //随时间施加重力 _verticalVelocity += Gravity * Time.deltaTime; } characterController.Move(moveDirection * speed * Time.deltaTime + new Vector3(0.0f, _verticalVelocity, 0.0f) * Time.deltaTime); } }
视角控制
public class MouseLook : MonoBehaviour { // 鼠标灵敏度 public float mouseSensitivity = 500f; // 玩家的身体Transform组件,用于旋转 public Transform playerBody; // x轴的旋转角度 float xRotation = 0f; void Start() { // 锁定光标到屏幕中心,并隐藏光标 Cursor.lockState = CursorLockMode.Locked; } // Update在每一帧调用 void Update() { // 执行自由视角查看功能 FreeLook(); } // 自由视角查看功能的实现 void FreeLook() { // 获取鼠标X轴和Y轴的移动量,乘以灵敏度和时间,得到平滑的移动速率 float mouseX = Input.GetAxis("Mouse X") * mouseSensitivity * Time.deltaTime; float mouseY = Input.GetAxis("Mouse Y") * mouseSensitivity * Time.deltaTime; //限制旋转角度在-90到90度之间,防止过度翻转 xRotation = Mathf.Clamp(xRotation, -90f, 90f); // 累计x轴上的旋转量 xRotation -= mouseY; // 应用摄像头的x轴旋转 transform.localRotation = Quaternion.Euler(xRotation, 0f, 0f); // 应用玩家身体的y轴旋转 playerBody.Rotate(Vector3.up * mouseX); } }
效果
简单的背包系统和物品交互
对UI知识还不太懂的小伙伴可以看这篇基础篇文件:【Unity游戏开发教程】零基础带你从小白到超神30——UI组件和布局的使用
绘制背包UI
物品插槽背景框
物品插槽,可以把物品插槽做出预制体,后面好修改
准星图像和文本
脚本控制
物品信息脚本
public class Item : MonoBehaviour { public new string name = "New Item";//物品名称 [TextArea] public string description = "New Description";//物品描述 public Sprite icon;//物品图标 public int currentQuantity = 1;//物品当前数量 public int maxQuantity = 16;//物品最大堆叠数量 }
背包插槽脚本
public class Slot : MonoBehaviour, IPointerEnterHandler, IPointerExitHandler { public bool hovered; // 鼠标是否悬停在该槽位上的标志 private Item heldItem; // 当前槽位持有的物品 private Color opaque = new Color(1, 1, 1, 1); // 不透明颜色 private Color transparent = new Color(1, 1, 1, 0); // 透明颜色 private Image thisSlotImage; // 该槽位的图像组件 public TMP_Text thisSlotQuantityText; // 用于显示物品数量的文本组件 // 初始化槽位 public void initialiseSlot() { thisSlotImage = gameObject.GetComponent<Image>(); thisSlotQuantityText = transform.GetChild(0).GetComponent<TMP_Text>(); thisSlotImage.sprite = null; thisSlotImage.color = transparent; setItem(null); } // 设置槽位中的物品 public void setItem(Item item) { heldItem = item; if (item != null) { thisSlotImage.sprite = heldItem.icon; thisSlotImage.color = opaque; updateData(); } else { thisSlotImage.sprite = null; thisSlotImage.color = transparent; updateData(); } } // 获取当前槽位持有的物品 public Item getItem() { return heldItem; } // 当前槽位是否持有的物品 public bool hasItem() { return heldItem ? true : false; } // 更新槽位显示的数据 public void updateData() { if (heldItem != null) // 如果持有物品 thisSlotQuantityText.text = heldItem.currentQuantity.ToString(); // 显示物品的数量 else // 如果不持有物品 thisSlotQuantityText.text = ""; } // 当鼠标指针进入槽位区域时调用 public void OnPointerEnter(PointerEventData pointerEventData) { hovered = true; } // 当鼠标指针离开槽位区域时调用 public void OnPointerExit(PointerEventData pointerEventData) { hovered = false; } }
库存系统脚本
public class Inventory : MonoBehaviour { [Header("UI")] public GameObject inventory; // 游戏中的背包界面 public List<Slot> allInventorySlots = new List<Slot>(); // 所有的槽位列表 public List<Slot> inventorySloats = new List<Slot>();//背包的的槽位列表 public Image crosshair; // 准星图像 public TMP_Text itemHoverText; // 当中心悬停在物品上时显示物品名称的文本 [Header("射线检测")] public float raycastDistance = 5f; // 射线检测的距离 public LayerMask itemLayer; // 射线检测的目标层,用于识别物品 public void Start() { toggleInventory(false); // 初始时关闭背包界面 //合并槽位 allInventorySlots.AddRange(inventorySloats); foreach (Slot uiSlot in allInventorySlots) // 初始化所有槽位 { uiSlot.initialiseSlot(); } } public void Update() { itemRaycast(Input.GetKeyDown(KeyCode.E)); // 显示物品名称和按E拾取物品 if (Input.GetKeyDown(KeyCode.Tab)) // 按下tab键切换背包界面的显示状态 toggleInventory(!inventory.activeInHierarchy); } private void itemRaycast(bool hasClicked = false) { itemHoverText.text = ""; // 默认不显示任何物品名称 Ray ray = Camera.main.ScreenPointToRay(crosshair.transform.position); // 从准星位置发出射线 RaycastHit hit; if (Physics.Raycast(ray, out hit, raycastDistance, itemLayer)) // 如果射线检测到物品层的对象 { if (hit.collider != null) { if (hasClicked) // 如果是按了操作,尝试捡起物品 { Item newItem = hit.collider.GetComponent<Item>(); if (newItem) { addItemToInventory(newItem); // 将物品添加到背包中 } } else // 否则,仅获取物品名称以显示 { Item newItem = hit.collider.GetComponent<Item>(); if (newItem) { itemHoverText.text = newItem.name; // 显示物品名称 } } } } } //将物品添加到背包中 private void addItemToInventory(Item itemToAdd) { int leftoverQuantity = itemToAdd.currentQuantity; // 剩余需要添加到背包的物品数量 Slot openSlot = null; // 记录一个空的槽位 for (int i = 0; i < allInventorySlots.Count; i++) // 遍历所有槽位 { Item heldItem = allInventorySlots[i].getItem(); if (heldItem != null && itemToAdd.name == heldItem.name) // 如果槽位中有相同名称的物品 { int freeSpaceInSlot = heldItem.maxQuantity - heldItem.currentQuantity; // 计算槽位中的剩余空间 if (freeSpaceInSlot >= leftoverQuantity) // 如果剩余空间足够 { heldItem.currentQuantity += leftoverQuantity; // 添加物品到该槽位 Destroy(itemToAdd.gameObject); // 销毁场景中的物品对象 allInventorySlots[i].updateData(); // 更新槽位显示的数据 return; } else // 如果剩余空间不足 { heldItem.currentQuantity = heldItem.maxQuantity; // 填满当前槽位 leftoverQuantity -= freeSpaceInSlot; // 更新剩余需要添加的物品数量 } } else if (heldItem == null) // 如果槽位为空 { if (!openSlot) openSlot = allInventorySlots[i]; // 记录第一个空槽位 } allInventorySlots[i].updateData(); // 更新槽位显示的数据 } if (leftoverQuantity > 0 && openSlot) // 如果还有剩余物品且找到了空槽位 { openSlot.setItem(itemToAdd); // 将物品添加到空槽位 itemToAdd.currentQuantity = leftoverQuantity; // 更新物品的数量 itemToAdd.gameObject.SetActive(false); // 隐藏场景中的物品对象 } else { itemToAdd.currentQuantity = leftoverQuantity; // 更新物品的数量 } } private void toggleInventory(bool enable) { //关闭背包时,关闭所有鼠标悬停在该槽位上的标志 if (!enable) { foreach (Slot curSlot in allInventorySlots) { curSlot.hovered = false; } } inventory.SetActive(enable); // 根据参数显示或隐藏背包界面 Cursor.lockState = enable ? CursorLockMode.None : CursorLockMode.Locked; // 根据背包界面的状态锁定或解锁鼠标指针 Cursor.visible = enable; // 设置鼠标指针的可见性 // 禁用或启用相机的旋转控制 Camera.main.GetComponent<MouseLook>().enabled = !enable; } }
物品挂载Item脚本,配置参数,记得添加碰撞体并修改图层为Item
背包插槽挂载Slot脚本
角色上挂载Inventory脚本
拾取
源码
源码不出意外的话我会放在最后一节