Unity3D水下场景与游泳系统开发指南
目录
最终效果
素材
将项目升级为URP
画一个水潭地形
材质升级为URP
创建水
调节水
第一人称人物移动控制
摄像机视角控制脚本 MouseLook
人物移动控制脚本 PlayerMovement
游泳
水面停留
添加水下后处理
水下呼吸
钓鱼
参考
最终效果
素材
将项目升级为URP
这里可以选择直接创建URP项目,也可以选择把普通项目升级为URP项目,关于如何升级,可以自行搜索,这里不赘述。
画一个水潭地形
材质升级为URP
PS:你可能会注意到即使转换材质后仍显示粉色,这其实是 Unity 的一个显示 Bug,实际上材质转换已经成功完成了。
创建水
新增空物体,添加Water Volume (Transforms)和Water Volume Helper组件配置参数
绑定水材质
添加子物体,并设置尺寸
调整一下水尺寸就显示出来了
调节水
将水调整合适大小,放置到刚才我们绘制的水潭地形上
可以调节水材质参数,达到自己想要的效果
ps:记得设置水的y轴高度,占满湖底
第一人称人物移动控制
摄像机视角控制脚本 MouseLook
public class MouseLook : MonoBehaviour
{public float mouseSensitivity = 1000f; // 鼠标灵敏度设置public Transform playerBody; // 玩家身体Transform组件private float xRotation = 0f; // 当前X轴旋转角度void Start(){Cursor.lockState = CursorLockMode.Locked; // 锁定并隐藏鼠标指针}void Update(){FreeLook(); // 每帧处理自由视角}void FreeLook(){// 计算平滑的鼠标移动量float mouseX = Input.GetAxis("Mouse X") * mouseSensitivity * Time.deltaTime;float mouseY = Input.GetAxis("Mouse Y") * mouseSensitivity * Time.deltaTime;// 更新并限制X轴旋转角度xRotation -= mouseY;xRotation = Mathf.Clamp(xRotation, -90f, 90f);// 应用摄像机旋转transform.localRotation = Quaternion.Euler(xRotation, 0f, 0f);playerBody.Rotate(Vector3.up * mouseX); // 旋转玩家身体}
}
人物移动控制脚本 PlayerMovement
using UnityEngine;[RequireComponent(typeof(CharacterController))]
public class PlayerMovement : MonoBehaviour
{public CharacterController characterController;public float walkSpeed = 6f; // 行走速度public float runSpeed = 9f; // 奔跑速度public float jumpHeight = 2.5f; // 跳跃高度private float Gravity = -19.8f; // 重力加速度private float horizontal;private float vertical;private Vector3 moveDirection; // 移动方向private float speed; // 当前速度private bool isRun; // 是否奔跑private bool isGround; // 是否在地面private float _verticalVelocity; // 垂直速度void Start(){speed = walkSpeed; // 初始化速度为行走速度}void Update(){// 获取输入horizontal = Input.GetAxis("Horizontal");vertical = Input.GetAxis("Vertical");// 状态检测与更新isGround = characterController.isGrounded;SetSpeed();SetRun();SetMove();SetJump();// 应用移动characterController.Move(moveDirection * speed * Time.deltaTime + new Vector3(0.0f, _verticalVelocity, 0.0f) * Time.deltaTime);}void SetSpeed(){speed = isRun ? runSpeed : walkSpeed; // 切换移动速度}void SetRun(){isRun = Input.GetKey(KeyCode.LeftShift); // 检测是否按下奔跑键}void SetMove(){// 计算并标准化移动方向moveDirection = (transform.right * horizontal + transform.forward * vertical).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;}}
}
效果
游泳
[RequireComponent(typeof(CharacterController))]
public class PlayerMovement : MonoBehaviour
{[Header("Components")][Tooltip("角色控制器")] public CharacterController characterController;[Header("Movement")][Tooltip("角色行走速度")] public float walkSpeed = 6f;[Tooltip("角色奔跑速度")] public float runSpeed = 9f;private float currentSpeed;private bool isRunning;private Vector3 moveDirection;[Header("Gravity")]private float gravity;private float verticalVelocity;[Tooltip("地面重力")] public float groundGravity = -19.8f;[Header("Jump")][Tooltip("跳跃高度")] public float jumpHeight = 5f;private bool isGrounded;[Header("Swimming")][Tooltip("是否在水中")] public bool isSwimming;[Tooltip("是否在水下")] public bool isUnderWater;[Tooltip("水中重力")] public float swimmingGravity = -0.5f;[Header("Camera")]public Transform cameraTransform;void Start(){currentSpeed = walkSpeed;}void Update(){float horizontal = Input.GetAxis("Horizontal");float vertical = Input.GetAxis("Vertical");isGrounded = characterController.isGrounded;UpdateSpeed();UpdateRunning();UpdateMovement(horizontal, vertical);}void UpdateSpeed(){currentSpeed = isRunning ? runSpeed : walkSpeed;}void UpdateRunning(){isRunning = Input.GetKey(KeyCode.LeftShift);}void UpdateMovement(float horizontal, float vertical){// 跳跃控制bool jump = Input.GetButtonDown("Jump");if (isGrounded){if (verticalVelocity < 0.0f){verticalVelocity = -2f;}if (jump){verticalVelocity = jumpHeight;}}// 水中移动逻辑if (isSwimming){gravity = swimmingGravity;verticalVelocity = gravity;moveDirection = transform.right * horizontal + cameraTransform.forward * vertical;}else // 地面移动逻辑{gravity = groundGravity;verticalVelocity += gravity * Time.deltaTime;moveDirection = transform.right * horizontal + transform.forward * vertical;}moveDirection = moveDirection.normalized;characterController.Move(moveDirection * currentSpeed * Time.deltaTime + new Vector3(0.0f, verticalVelocity, 0.0f) * Time.deltaTime);}
}
记得配置角色标签
编辑水的触发器碰撞体积
效果
水面停留
当角色离开水面时,需要停止其上下浮动行为。可以在角色离开水面时将Y轴速度归零来实现这一效果。以下是修改后的PlayerMovement代码:
public bool isUnderWater; // 判断角色是否在水下void SetJump()
{bool jump = Input.GetButtonDown("Jump");if (isGround){// 着陆时重置垂直速度if (_verticalVelocity < 0.0f){_verticalVelocity = -2f;}if (jump){_verticalVelocity = jumpHeight;}}// 水中移动处理if (isSwimming){if (isUnderWater){Gravity = swimmingGravity;_verticalVelocity = Gravity;}else{_verticalVelocity = 0; // 离开水面时停止浮动}moveDirection = transform.right * horizontal + Camera.forward * vertical;}else{Gravity = groundGravity;_verticalVelocity += Gravity * Time.deltaTime;moveDirection = transform.right * horizontal + transform.forward * vertical;}moveDirection = moveDirection.normalized; // 归一化移动方向characterController.Move(moveDirection * speed * Time.deltaTime + new Vector3(0.0f, _verticalVelocity, 0.0f) * Time.deltaTime);
}
我们只要检测摄像机是否在水下即可,给我们的摄像机添加触发器和刚体
修改SwimAra
public class SwimAra : MonoBehaviour {private void OnTriggerEnter(Collider other) {if(other.CompareTag("Player")){other.GetComponent<PlayerMovement>().isSwimming = true;}if(other.CompareTag("MainCamera")){other.GetComponentInParent<PlayerMovement>().isUnderWater = true;}}private void OnTriggerExit(Collider other) {if(other.CompareTag("Player")){other.GetComponent<PlayerMovement>().isSwimming = false;}if(other.CompareTag("MainCamera")){other.GetComponentInParent<PlayerMovement>().isUnderWater = false;}} }
效果
添加水下后处理
修改模式为局部,碰撞体积设置和水体一样大
简单配置后处理,添加通道混色器
你会发现看不到效果,因为我们还需要开启摄像机的后处理效果
,记得所有相机都要开启,记得把人物放进水里
提升伽马增益
视野模糊效果(Depth OF Field)
全屏屏幕光圈效果
胶片颗粒感
效果
水下呼吸
新增PlayerHealth,控制人物状态:
public class PlayerHealth : MonoBehaviour
{public static PlayerHealth Instance;public float maxHealth = 100;//最大生命值public float currentHealth;//---玩家氧气----/public float currentOxygenPercent; // 当前氧气百分比public float maxOxygenPercent = 100; // 最大氧气百分比public float oxygenDecreasedPerSecond = 1f; // 每次减少的氧气百分比private float oxygenTimer = 0f; // 氧气计时器private float decreaseInterval = 1f; // 减少间隔public GameObject oxygenBar;//氧气条private void Awake() {Instance = this;}void Start(){currentHealth = maxHealth;currentOxygenPercent = maxOxygenPercent;}void Update(){if (GetComponent<PlayerMovement>().isUnderWater){oxygenBar.SetActive(true);oxygenTimer += Time.deltaTime;if (oxygenTimer > decreaseInterval){DecreaseOxygen();oxygenTimer = 0;}}else{oxygenBar.SetActive(false);currentOxygenPercent = maxOxygenPercent;}}private void DecreaseOxygen(){currentOxygenPercent -= oxygenDecreasedPerSecond;// 没有氧气了if (currentOxygenPercent < 0){currentOxygenPercent = 0;//扣血currentHealth -= 1f;}}
}
新增OxygenBar,控制人物氧气条UI:
public class OxygenBar : MonoBehaviour
{private Slider slider; // 氧气条的滑动条public TextMeshProUGUI oxygenCounter; // 氧气计数器文本private float currentOxygen, maxOxygen; // 当前氧气值和最大氧气值void Awake(){slider = GetComponent<Slider>(); // 获取滑动条组件}void Update(){currentOxygen = PlayerHealth.Instance.currentOxygenPercent; // 获取当前氧气百分比maxOxygen = PlayerHealth.Instance.maxOxygenPercent; // 获取最大氧气百分比float fillValue = currentOxygen / maxOxygen; // 计算填充值slider.value = fillValue; // 更新滑动条的值oxygenCounter.text = (fillValue * 100).ToString("0") + "%"; // 更新氧气计数器文本显示}
}
配置
效果
钓鱼
using UnityEngine;
using System.Collections;public class FishingManager : MonoBehaviour
{public Transform hookTransform; // 鱼钩的Transformpublic float throwForce = 10f; // 抛竿的力public float waterLevel = 0f; // 水平面的高度public float biteWaitTimeMin = 2f; // 鱼咬钩的最小等待时间public float biteWaitTimeMax = 10f; // 鱼咬钩的最大等待时间public float biteReactionTime = 2f; // 咬钩后玩家反应的时间(超过这个时间则鱼逃跑)public KeyCode throwKey = KeyCode.Space; // 抛竿按键public KeyCode reelKey = KeyCode.Space; // 收竿按键private bool isThrown = false; // 是否已经抛竿private bool isInWater = false; // 鱼钩是否在水中private bool isFishBiting = false; // 鱼是否正在咬钩private bool isReeling = false; // 是否正在收竿private float biteTimer = 0f; // 咬钩计时器private float reactionTimer = 0f; // 反应计时器void Update(){// 抛竿if (!isThrown && Input.GetKeyDown(throwKey)){ThrowHook();}// 如果鱼钩在水中且没有鱼咬钩,则等待咬钩if (isInWater && !isFishBiting && !isReeling){// 等待随机时间后鱼咬钩biteTimer -= Time.deltaTime;if (biteTimer <= 0){StartBite();}}// 如果鱼正在咬钩,等待玩家收竿if (isFishBiting){reactionTimer -= Time.deltaTime;if (reactionTimer <= 0){// 鱼逃跑FishEscaped();}else if (Input.GetKeyDown(reelKey)){// 玩家收竿ReelFish();}}}// 抛竿方法void ThrowHook(){// 这里简单模拟抛竿:给鱼钩一个向前的力Rigidbody hookRb = hookTransform.GetComponent<Rigidbody>();if (hookRb != null){hookRb.AddForce(transform.forward * throwForce, ForceMode.Impulse);isThrown = true;isInWater = false; // 刚抛出时不在水中}}// 当鱼钩进入水中(触发器)void OnTriggerEnter(Collider other){if (other.CompareTag("Water")){isInWater = true;// 设置一个随机的咬钩等待时间biteTimer = Random.Range(biteWaitTimeMin, biteWaitTimeMax);}}// 开始咬钩void StartBite(){isFishBiting = true;reactionTimer = biteReactionTime; // 重置反应计时器// 这里可以触发咬钩的视觉效果或声音Debug.Log("鱼咬钩了!快收竿!");}// 收竿钓到鱼void ReelFish(){isReeling = true;isFishBiting = false;// 钓到鱼后的处理,比如增加鱼的数量,播放动画等Debug.Log("恭喜!钓到一条鱼!");// 重置状态,准备下一次抛竿ResetFishing();}// 鱼逃跑void FishEscaped(){isFishBiting = false;Debug.Log("鱼逃跑了...");ResetFishing();}// 重置钓鱼状态void ResetFishing(){isThrown = false;isInWater = false;isReeling = false;// 将鱼钩拉回原位(实际游戏中可能需要一个动画过程)// 这里简单重置位置hookTransform.position = transform.position;Rigidbody hookRb = hookTransform.GetComponent<Rigidbody>();if (hookRb != null){hookRb.velocity = Vector3.zero;hookRb.angularVelocity = Vector3.zero;}}
}
- 将脚本挂载在玩家角色上(或一个管理物体上)。
- 将鱼钩物体拖拽到hookTransform变量上。
- 确保鱼钩有刚体组件(Rigidbody)和碰撞器(Collider)。
- 创建一个代表水的物体,设置Tag为"Water",并添加Collider(设置为触发器Trigger),调整到合适的位置和大小。
参考
https://www.youtube.com/watch?v=vX5AOF4Wdgo&list=PLtLToKUhgzwnk4U2eQYridNnObc2gqWo-&index=44