当前位置: 首页 > news >正文

Unity3D水下场景与游泳系统开发指南

目录

最终效果

素材

将项目升级为URP

画一个水潭地形

材质升级为URP

创建水

调节水

第一人称人物移动控制

摄像机视角控制脚本 MouseLook

人物移动控制脚本 PlayerMovement

游泳

水面停留

添加水下后处理

水下呼吸

钓鱼

参考


最终效果

image.png

素材

image

将项目升级为URP

这里可以选择直接创建URP项目,也可以选择把普通项目升级为URP项目,关于如何升级,可以自行搜索,这里不赘述。

画一个水潭地形

image

材质升级为URP

image

PS:你可能会注意到即使转换材质后仍显示粉色,这其实是 Unity 的一个显示 Bug,实际上材质转换已经成功完成了。

创建水

新增空物体,添加Water Volume (Transforms)和Water Volume Helper组件配置参数

image

绑定水材质

image

添加子物体,并设置尺寸

image

调整一下水尺寸就显示出来了

image

调节水

将水调整合适大小,放置到刚才我们绘制的水潭地形上

image

可以调节水材质参数,达到自己想要的效果

image

ps:记得设置水的y轴高度,占满湖底

image

第一人称人物移动控制

image

image

image

image

摄像机视角控制脚本 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;}}
}

效果

image

游泳

[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);}
}

 

记得配置角色标签

image

编辑水的触发器碰撞体积

image

效果

水面停留

当角色离开水面时,需要停止其上下浮动行为。可以在角色离开水面时将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);
}

我们只要检测摄像机是否在水下即可,给我们的摄像机添加触发器和刚体

image

修改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;}}
}

效果

image.png

添加水下后处理

image

修改模式为局部,碰撞体积设置和水体一样大

image

简单配置后处理,添加通道混色器

image

你会发现看不到效果,因为我们还需要开启摄像机的后处理效果,记得所有相机都要开启,记得把人物放进水里

image

提升伽马增益

image

视野模糊效果(Depth OF Field)

image

全屏屏幕光圈效果

image

胶片颗粒感

image

效果

image.png

水下呼吸

新增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") + "%"; // 更新氧气计数器文本显示}
}

配置

image

image

效果

image.png

钓鱼

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;}}
}

  1. 将脚本挂载在玩家角色上(或一个管理物体上)。
  2. 将鱼钩物体拖拽到hookTransform变量上。
  3. 确保鱼钩有刚体组件(Rigidbody)和碰撞器(Collider)。
  4. 创建一个代表水的物体,设置Tag为"Water",并添加Collider(设置为触发器Trigger),调整到合适的位置和大小。

参考

https://www.youtube.com/watch?v=vX5AOF4Wdgo&list=PLtLToKUhgzwnk4U2eQYridNnObc2gqWo-&index=44

http://www.dtcms.com/a/318615.html

相关文章:

  • ubuntu18.04在fstab文件中挂载硬盘失败,系统进入紧急模式的解决方法
  • js 从 json 中取 key 的值
  • 云平台托管集群:EKS、GKE、AKS 深度解析与选型指南-第一章
  • 磁悬浮转子变转速工况下的振动抑制全解析
  • 什么是「回调函数」 Callback Function ?
  • Linux(17)——Linux进程信号(上)
  • 28.(vue3.x+vite)el-pagination中文设置(兼容其他elementPlus组件)
  • PaddleOCR 多线程并发问题
  • K8S命令记录
  • 利用多线程设计群ping工具
  • 5G随身WiFi怎么选?实测延迟/网速/续航,中兴V50适合商务,格行MT700适合短租、户外党~避坑指南+适用场景全解析
  • 无监督学习之K-means算法
  • 古多倍体化对被子植物适应性进化的遗传贡献--文献精度154
  • 本地部署 SQLite 数据库管理工具 SQLite Browser ( Web ) 并实现外部访问
  • 根据经纬度(从nc格式环境数据文件中)提取环境因子
  • RabbitMQ面试精讲 Day 12:镜像队列与Quorum队列对比
  • PCL 平面特征点提取
  • 2 SpringBoot项目对接单点登录说明
  • C语言控制语句练习题3
  • 数据结构与算法
  • 嵌入式 - 数据结构:栈和队列
  • [Oracle] ROUND()函数
  • 软件架构:系统结构的顶层设计与战略约束
  • 【前端】Vite中import.meta功能详解
  • 【多模态微调】【从0开始】Qwen2-VL + llamafactory
  • 小杰python高级(one day)——numpy库
  • 应急响应-windows篇
  • Spring选择哪种方式代理?
  • 12、Docker Compose 安装 Redis
  • CGAL Kernel 和 Traits 类深度解析:从官方教程到实践应用