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

Unity实现Root Motion动画的Navigation自动导航

Root motion动画可以将角色的根节点(通常是角色的骨盆或脚部)的运动直接应用到游戏对象上,从而实现角色的自然移动和旋转,避免出现脚底打滑的现象。采用Root motion动画的游戏对象,通常是重载了onAnimatorMove函数,在脚本中来设置动画的速度,从而实现角色的移动。Unity的Navigation系统是一个用于实现游戏世界中的寻路和导航功能的组件。它允许游戏角色在复杂的游戏环境中自动找到从一点到另一点的最短路径。如果我们对采用Root motion动画的游戏对象应用Navigation,就会产生冲突,因为这两个组件都会尝试控制游戏对象的移动。有两个解决方式:

一是让动画跟随Navigation agent,通过获取agent.velocity来设置root motion的速度,从而大致匹配Agent的移动到动画的移动。这个方式最简单,但是可能会出现脚底打滑的现象。

二是让Agent跟随动画,关闭agent的updatePosition和updateRotation,通过计算agent的nextPosition和动画根节点的rootPosition的插值来进行控制。这种方式比方式一要复杂,但是效果更好。以下将以一个游戏场景为例子,详细介绍一下如何实现方式二。

游戏场景

在游戏中,对于NPC角色,当前设置了几个状态,分别是漫游Wander,瞄准Aim以及追踪Chase。NPC刚开始是漫游状态,在场景中自由地进行移动,这时是通过Root motion来驱动的。当NPC检测到玩家时,会进入瞄准状态。如果玩家进行躲避NPC,则NPC会进入追踪状态,自动跑到上一次发现玩家的位置,这时NPC是由Navigation来驱动,实现自动寻路。可见对于NPC是需要按照不同的场景来用Root motion或Navigation来驱动的。

Animator设置

建立一个名为Enemy的Animator,包含了两个状态,分别是Aim和Move,设置如下:

添加两个Trigger,分别为Aim和Walk,用于切换状态。定义一个名为Speed的Float变量,用于控制Root motion的移动速度。

Move状态是一个BlendTree,通过Speed来进行Idle,Walk,Run这三种动作的混合,改变Speed的值,可以看到人物动作的改变。

Unity Blendtree动画

改变Speed的值,可以看到人物的动作的改变。

实现漫游状态

现在给游戏对象增加一个名为EnemyAI的脚本文件,实现游戏对象在场景中漫游。代码如下:

public class EnemyAI : MonoBehaviour
{[Header("Enemy eyeview")]public float eyeviewDistance = 500.0f;public float viewAngle = 120f;public float obstacleRange = 3.0f;[Header("Enemy Property")]public float enemyHeight = 1.8f;public float enemyWidth = 1.2f;public float rotateSpeed = 2.0f;public float maxDetectDistance = 10f;private float _walkSpeed = 1.5f;private float _runSpeed = 3.5f;private Animator _animator;private Transform _transform;private float _currentSpeed;private float _targetSpeed;private float _statusDuration = 1.0f;private bool _isStatusTimerEnds = true;private bool _isDetectTimerEnds = true;[Flags]private enum EnemyStatus {Aim,Shoot,Wander,Chase}private EnemyStatus _enemyStatus;void Start(){_animator = GetComponent<Animator>();_rb = GetComponent<Rigidbody>();_transform = transform;_enemyStatus = EnemyStatus.Wander;rayCastOffset = new Vector3(0f, enemyHeight - 0.6f, 0f);}void Update(){if (_enemyStatus == EnemyStatus.Wander) {Wander();} Detect();}private void OnAnimatorMove() {if (_currentSpeed != _targetSpeed) {if (Mathf.Abs(_currentSpeed - _targetSpeed) > 0.1) {_currentSpeed = Mathf.Lerp(_currentSpeed, _targetSpeed, 0.5f);} else {_currentSpeed = _targetSpeed;} }_animator.SetFloat("Speed", _currentSpeed); Vector3 speed = new Vector3(_animator.velocity.x, _rb.velocity.y, _animator.velocity.z);_rb.velocity = speed;}void Wander() {if (_isStatusTimerEnds) {_targetSpeed = UnityEngine.Random.Range(0, 2) == 0 ? 0f : _walkSpeed;_statusDuration = UnityEngine.Random.Range(5f, 10f);_isStatusTimerEnds = false;StartCoroutine(StatusTimer());}}IEnumerator StatusTimer() {float timer = 0;while (timer < _statusDuration) {timer += Time.deltaTime;yield return null; }_isStatusTimerEnds = true;}IEnumerator DetectTimer() {float timer = 0;while (timer < _detectDuration) {timer += Time.deltaTime;yield return null; }_isDetectTimerEnds = true;}float DetectObstacle(float angle) {RaycastHit hit;int layerMask = ~(1 << 8);Quaternion rotation = Quaternion.AngleAxis(angle, Vector3.up);bool hitDetect = Physics.BoxCast(_transform.position + rayCastOffset, new Vector3(enemyWidth/2, rayCastOffset.y/2, 0.2f), rotation * _transform.forward, out hit,transform.rotation * rotation,maxDetectDistance,layerMask);if (hitDetect) {return hit.distance;} else {return 9999.0f;}}void Detect() {if (_isDetectTimerEnds) {_isDetectTimerEnds = false;StartCoroutine(DetectTimer());if (_currentSpeed > 0.2) {float distance = DetectObstacle(0f);if (distance < obstacleRange) {float leftDistance = DetectObstacle(-90f);float rightDistance = DetectObstacle(90f);float startAngle = -45f;float endAngle = -110f;if (rightDistance < obstacleRange && leftDistance < obstacleRange) {startAngle = 180f;endAngle = 180.01f;} else {if (leftDistance < rightDistance) {startAngle *= -1f;endAngle *= -1f;} }_targetAngle = UnityEngine.Random.Range(startAngle, endAngle);_currentAngle = 0f;}}} else {if (Mathf.Abs(_currentAngle - _targetAngle) > 0.1) {_prevAngle = _currentAngle;_currentAngle = Mathf.Lerp(_currentAngle, _targetAngle, rotateSpeed * Time.deltaTime);_transform.Rotate(0, _currentAngle - _prevAngle, 0);} else {_currentAngle = _targetAngle;}}}
}

以上代码大致逻辑是一开始设置状态为漫游状态,然后通过一个StatusTimer来计时,每次计时器到时就随机设置一个速度值。在onAnimatorMove函数中通过插值的方法来平滑改变速度值,并设置Animator的speed值,实现通过root motion动画来驱动游戏对象。另外还设置一个DetectTimer来计时,定期调用DetectObstacle函数来检测游戏对象行进方向上是否有障碍物,如有则进行随机转向。运行场景,可以看到游戏对象在场景中可以自由地进行漫步。

实现瞄准状态

现在我们要增加一个检测玩家的功能,让游戏对象在漫步过程中能发现玩家,并且进入瞄准状态。对以上代码做改动

public class EnemyAI : MonoBehaviour
{...void Update(){if (_enemyStatus == EnemyStatus.Aim) {_prevAngle = _currentAngle;_currentAngle = Mathf.Lerp(_currentAngle, _targetAngle, rotateSpeed * Time.deltaTime);_transform.Rotate(0, _currentAngle - _prevAngle, 0);if (Mathf.Abs(_currentAngle - _targetAngle) < 0.5) {_currentAngle = _targetAngle;}} ...}bool DetectPlayer() {bool findPlayer = false;Vector3 position = _transform.position + new Vector3(0f, enemyHeight-0.2f, 0f);_spottedPlayers = Physics.OverlapSphere(position, eyeviewDistance, LayerMask.GetMask("Character"));for (int i=0;i<_spottedPlayers.Length;i++) {Vector3 playerPosition = _spottedPlayers[i].transform.position;float angle = Vector3.SignedAngle(transform.forward, playerPosition - position, Vector3.up);if (angle <= viewAngle/2 && angle >= -viewAngle/2) {RaycastHit info;int layermask = LayerMask.GetMask("Character", "Default");Physics.Raycast(position, playerPosition - position, out info, eyeviewDistance, layermask);if (info.collider == _spottedPlayers[i]) {if (_currentSpeed >= 0.1) {_targetSpeed = 0f;_currentSpeed = Mathf.Lerp(_currentSpeed, _targetSpeed, 0.75f);_animator.SetFloat("Speed", _currentSpeed);} else {_prevPlayerPosition = playerPosition;_foundPlayer = true;_enemyStatus = EnemyStatus.Aim;_animator.SetTrigger("Aim");_currentAngle = 0;_targetAngle = angle;_currentSpeed = 0f;findPlayer = true;}}}}return findPlayer;} void Detect() {if (_isDetectTimerEnds) {...DetectPlayer()}...}
}

在原来的Detect代码中增加一个对检测玩家的DetectPlayer的调用,当检测到玩家时,设置状态为Aim,并且设置Animator的Aim触发器,播放瞄准动作。

实现追踪状态

当游戏对象检测到玩家之后,玩家可以躲避游戏对象的瞄准,例如跑到一旁的障碍物隐藏。游戏对象找不到玩家,这时应该跑去之前发现玩家的地方,进行搜索。要实现这个功能,简单的一个想法是通过Unity的Navigation自动寻路功能来实现,让游戏对象自行寻路,而不是通过代码来控制。但是如前面提到的,Navigation和Root motion同时驱动游戏对象就会产生冲突,因此我们可以采取方式二来解决,即让Navigation agent跟随动画来移动。

给游戏对象增加一个Navmesh agent组件,然后对代码进行如下改动:

public class EnemyAI : MonoBehaviour
{...private NavMeshAgent _agent;Vector2 smoothDeltaPosition = Vector2.zero;Vector2 velocity = Vector2.zero;void Start(){..._agent = GetComponent<NavMeshAgent>();_agent.updatePosition = false;_agent.speed = _runSpeed;}void Update(){...if (_enemyStatus == EnemyStatus.Chase) {Vector3 worldDeltaPosition = _agent.nextPosition - _transform.position;// Map 'worldDeltaPosition' to local spacefloat dx = Vector3.Dot(_transform.right, worldDeltaPosition);float dy = Vector3.Dot(_transform.forward, worldDeltaPosition);Vector2 deltaPosition = new Vector2(dx, dy);// Low-pass filter the deltaMovefloat smooth = Mathf.Min(1.0f, Time.deltaTime/0.15f);smoothDeltaPosition = Vector2.Lerp (smoothDeltaPosition, deltaPosition, smooth);// Update velocity if time advancesif (Time.deltaTime > 1e-5f)velocity = smoothDeltaPosition / Time.deltaTime;//Debug.LogFormat("Chase, speed:{0}", velocity.magnitude);_animator.SetFloat("Speed", velocity.magnitude); _transform.LookAt(_agent.steeringTarget + transform.forward);if (_agent.remainingDistance < _agent.radius) {_enemyStatus = EnemyStatus.Wander;}}Detect()}private void OnAnimatorMove() {if (_enemyStatus == EnemyStatus.Chase) {_transform.position = _agent.nextPosition;}else {if (_currentSpeed != _targetSpeed) {if (Mathf.Abs(_currentSpeed - _targetSpeed) > 0.1) {_currentSpeed = Mathf.Lerp(_currentSpeed, _targetSpeed, 0.5f);} else {_currentSpeed = _targetSpeed;} }_animator.SetFloat("Speed", _currentSpeed); Vector3 speed = new Vector3(_animator.velocity.x, _rb.velocity.y, _animator.velocity.z);_rb.velocity = speed;}}void Detect() {if (_isDetectTimerEnds) {...//DetectPlayer();if (!DetectPlayer()) {if (_foundPlayer) {_foundPlayer = false;_agent.nextPosition = _transform.position;_agent.destination = _prevPlayerPosition;_enemyStatus = EnemyStatus.Chase;_animator.SetTrigger("Walk");_targetSpeed = _runSpeed;}} ...}
}

以上的代码值得详细讲解一下,在Start函数中,设置了agent的updatePosition为false,即不让agent来移动游戏对象,同时设置agent的最大速度不要超过runspeed。在Update函数中,判断如果当前是Chase状态,那么计算agent的nextPosition与当前位置的差值,然后计算在deltaTime时间间隔中,需要以什么速度来移动,并设置animator的speed,使得游戏对象的动作与移动速度保持同步,不会出现脚底打滑的现象。在onAnimatorMove函数中,通过设置transform的位置为agent的nextPosition来实现移动。在Detect函数中进行修改,如果之前发现玩家,但现在没有发现,则进入Chase状态, 把之前发现玩家的位置设置为agent的目的地,让agent来进行自动寻路。注意在进入Chase状态时需要更新一下agent的nextPosition为当前游戏对象的位置,因为我们之前设置了updatePostion为false,所以agent的当前位置并不同步。

实现效果

Root Motion动画与Navigation结合

FPS教程

另外我之前也写了一系列文章介绍如何实现FPS游戏,有兴趣的可以了解一下

Unity开发一个FPS游戏_unity 模仿开发fps 游戏-CSDN博客

Unity开发一个FPS游戏之二_unity 模仿开发fps 游戏-CSDN博客

Unity开发一个FPS游戏之三-CSDN博客

Unity开发一个FPS游戏之四_unity fps-CSDN博客

Unity开发一个FPS游戏之五-CSDN博客

相关文章:

Unity实现Root Motion动画的Navigation自动导航

Root motion动画可以将角色的根节点&#xff08;通常是角色的骨盆或脚部&#xff09;的运动直接应用到游戏对象上&#xff0c;从而实现角色的自然移动和旋转&#xff0c;避免出现脚底打滑的现象。采用Root motion动画的游戏对象&#xff0c;通常是重载了onAnimatorMove函数&…...

[react]不能将类型“string | undefined”分配给类型“To”。 不能将类型“undefined”分配给类型“To”

场景, 封装组件的时候, 想通过外部传进去一个路由地址, 再用<Link to{}>跳转, 显示这个, 有四种方法解决 第一种 合并运算符 ?? ?? 是 空值合并运算符&#xff08;Nullish Coalescing Operator&#xff09;&#xff0c;它是 JavaScript 和 TypeScript 中的一种逻辑…...

python实现基于RPC协议的接口自动化测试

01 什么是RPC RPC&#xff08;Remote Procedure Call&#xff09;远程过程调用协议是一个用于建立适当框架的协议。从本质上讲&#xff0c;它使一台机器上的程序能够调用另一台机器上的子程序&#xff0c;而不会意识到它是远程的。 RPC 是一种软件通信协议&#xff0c;一个程…...

如何使用PSQL Tool还原pg数据库(sql格式)

新建一个数据库用来还原&#xff1b;选择新建的数据库&#xff0c;右键选择【PSQL Tool】&#xff0c;打开PSQL Tool命令行界面&#xff1b;赋予pg库对sql文件的执行权限&#xff0c;否则会报“Permission denied”的错误&#xff0c;命令如下&#xff1a; chmod urwx D://NoS…...

uni-app商品搜索页面

目录 一:功能概述 二:功能实现 一:功能概述 商品搜索页面,可以根据商品品牌,商品分类,商品价格等信息实现商品搜索和列表展示。 二:功能实现 1:商品搜索数据 <view class="search-map padding-main bg-base"> <view class…...

【深度学习】零基础介绍循环神经网络(RNN)

RNN介绍 零基础介绍语言处理技术基本介绍分词算法词法分析工具文本分类与聚类情感分析 自然语言处理词向量词向量学习模型1. 神经网络语言模型2. CBOW 和 skip-gram3. 层次化softmax方法4. 负采样方法 RNN介绍RNN的变种&#xff1a;LSTM1. Forget Gate2. Input Gate3. Update M…...

青少年编程与数学 02-004 Go语言Web编程 13课题、模板引擎

青少年编程与数学 02-004 Go语言Web编程 13课题、模板引擎 一、模板引擎模板引擎的主要特点包括&#xff1a;模板引擎的应用场景&#xff1a;Go语言中的模板引擎&#xff1a;示例&#xff1a;使用Go的html/template包 二、工作流程1. 创建模板文件2. 准备数据3. 加载模板4. 渲染…...

如何优雅的关闭GoWeb服务器

以下内容均为Let’s Go Further内容节选以及作者本人理解。 这里创建了一个后台进程用于捕获关闭信号&#xff0c;在后台进程中&#xff0c;主要内容为&#xff1a; 创建一个缓冲通道 quit使用signal.Notify函数监听并捕获关机信号SIGINT,SIGTERM&#xff0c;在捕获关机信号后…...

AI程序员,开源的Devin,OpenHands 如何使用HuggingFace Inference API

我用了一下&#xff0c;界面这样子&#xff1a; Github&#xff1a;https://github.com/All-Hands-AI/OpenHands OpenHands 如何使用HuggingFace Inference API huggingface/meta-llama/Llama-3.3-70B-Instruct 而不是 meta-llama/Llama-3.3-70B-Instruct 不要设置base URL&…...

【动手学运动规划】 5.2 数值优化基础:梯度下降法,牛顿法

朕四季常服, 不过八套. — 大明王朝1566 道长 &#x1f3f0;代码及环境配置&#xff1a;请参考 环境配置和代码运行! 上一节我们介绍了数值优化的基本概念, 让大家对最优化问题有了基本的理解. 那么对于一个具体的问题, 我们应该如何求解呢? 这一节我们将介绍几个基本的求解…...

电子应用设计方案66:智能打印机系统设计

智能打印机系统设计 一、引言 随着科技的不断发展&#xff0c;打印机也在向智能化方向演进。智能打印机不仅能够提供高质量的打印服务&#xff0c;还具备便捷的操作、智能的管理和连接功能。 二、系统概述 1. 系统目标 - 实现高效、高质量的打印输出。 - 支持多种连接方式&am…...

iClient3D for Cesium 实现限高分析

作者&#xff1a;gaogy 1、背景 随着地理信息技术的发展&#xff0c;三维地球技术逐渐成为了许多领域中的核心工具&#xff0c;尤其是在城市规划、环境监测、航空航天以及军事领域。三维地图和场景的应用正在帮助人们更加直观地理解空间数据&#xff0c;提供更高效的决策支持。…...

AI开发:使用支持向量机(SVM)进行文本情感分析训练 - Python

支持向量机是AI开发中最常见的一种算法。之前我们已经一起初步了解了它的概念和应用&#xff0c;今天我们用它来进行一次文本情感分析训练。 一、概念温习 支持向量机&#xff08;SVM&#xff09;是一种监督学习算法&#xff0c;广泛用于分类和回归问题。 它的核心思想是通过…...

torch.unsqueeze:灵活调整张量维度的利器

在深度学习框架PyTorch中&#xff0c;张量&#xff08;Tensor&#xff09;是最基本的数据结构&#xff0c;它类似于NumPy中的数组&#xff0c;但可以在GPU上运行。在日常的深度学习编程中&#xff0c;我们经常需要调整张量的维度以适应不同的操作和层。torch.unsqueeze函数就是…...

【WRF教程第3.1期】预处理系统 WPS 详解:以4.5版本为例

预处理系统 WPS 详解&#xff1a;以4.5版本为例 每个 WPS 程序的功能程序1&#xff1a;geogrid程序2&#xff1a;ungrib程序3&#xff1a;metgrid WPS运行&#xff08;Running the WPS&#xff09;步骤1&#xff1a;Define model domains with geogrid步骤2&#xff1a;Extract…...

SD ComfyUI工作流 根据图像生成线稿草图

文章目录 线稿草图生成SD模型Node节点工作流程工作流下载效果展示线稿草图生成 该工作流的设计目标是将输入的图像转换为高质量的线稿风格输出。其主要流程基于 Stable Diffusion 技术,结合文本和图像条件,精确生成符合预期的线条艺术图像。工作流的核心是通过模型的条件设置…...

挑战一个月基本掌握C++(第六天)了解函数,数字,数组,字符串

一 C函数 函数是一组一起执行一个任务的语句。每个 C 程序都至少有一个函数&#xff0c;即主函数 main() &#xff0c;所有简单的程序都可以定义其他额外的函数。 您可以把代码划分到不同的函数中。如何划分代码到不同的函数中是由您来决定的&#xff0c;但在逻辑上&#xff…...

git中的多人协作

目录 1.1多人协作1.1.1创建仓库1.1.2协作处理1.1.3冲突处理 1.2分支推送协作1.3分支拉取协作1.4远程分支的删除 1.1多人协作 1.1.1创建仓库 新建两个文件夹&#xff0c;不需要初始化为git仓库&#xff0c;直接克隆远程仓库命名testGit1&#xff0c;testGit2 指定本地仓库级别…...

解决新安装CentOS 7系统mirrorlist.centos.org can‘t resolve问题

原因 mirrorlist.centos.org yum源用不了 解决办法就是 # cd /etc/yum.repos.d/ # mv CentOS-Base.repo CentOS-Base.repo_bak # vim CentOS-Base.repoCentOS系统操作 # mv /etc/yum.repos.d/*.repo /etc/yum.repos.d/*.repo_bak # curl -o /etc/yum.repos.d/CentOS-Linux-Ba…...

RK3588 , mpp硬编码yuv, 保存MP4视频文件.

RK3588 , mpp硬编码yuv, 保存MP4视频文件. ⚡️ 传送 ➡️ Ubuntu x64 架构, 交叉编译aarch64 FFmpeg mppRK3588, FFmpeg 拉流 RTSP, mpp 硬解码转RGBRk3588 FFmpeg 拉流 RTSP, 硬解码转RGBRK3588 , mpp硬编码yuv, 保存MP4视频文件....

vscode里如何用git

打开vs终端执行如下&#xff1a; 1 初始化 Git 仓库&#xff08;如果尚未初始化&#xff09; git init 2 添加文件到 Git 仓库 git add . 3 使用 git commit 命令来提交你的更改。确保在提交时加上一个有用的消息。 git commit -m "备注信息" 4 …...

【机器视觉】单目测距——运动结构恢复

ps&#xff1a;图是随便找的&#xff0c;为了凑个封面 前言 在前面对光流法进行进一步改进&#xff0c;希望将2D光流推广至3D场景流时&#xff0c;发现2D转3D过程中存在尺度歧义问题&#xff0c;需要补全摄像头拍摄图像中缺失的深度信息&#xff0c;否则解空间不收敛&#xf…...

TRS收益互换:跨境资本流动的金融创新工具与系统化解决方案

一、TRS收益互换的本质与业务逻辑 &#xff08;一&#xff09;概念解析 TRS&#xff08;Total Return Swap&#xff09;收益互换是一种金融衍生工具&#xff0c;指交易双方约定在未来一定期限内&#xff0c;基于特定资产或指数的表现进行现金流交换的协议。其核心特征包括&am…...

CMake 从 GitHub 下载第三方库并使用

有时我们希望直接使用 GitHub 上的开源库,而不想手动下载、编译和安装。 可以利用 CMake 提供的 FetchContent 模块来实现自动下载、构建和链接第三方库。 FetchContent 命令官方文档✅ 示例代码 我们将以 fmt 这个流行的格式化库为例,演示如何: 使用 FetchContent 从 GitH…...

根据万维钢·精英日课6的内容,使用AI(2025)可以参考以下方法:

根据万维钢精英日课6的内容&#xff0c;使用AI&#xff08;2025&#xff09;可以参考以下方法&#xff1a; 四个洞见 模型已经比人聪明&#xff1a;以ChatGPT o3为代表的AI非常强大&#xff0c;能运用高级理论解释道理、引用最新学术论文&#xff0c;生成对顶尖科学家都有用的…...

Mysql8 忘记密码重置,以及问题解决

1.使用免密登录 找到配置MySQL文件&#xff0c;我的文件路径是/etc/mysql/my.cnf&#xff0c;有的人的是/etc/mysql/mysql.cnf 在里最后加入 skip-grant-tables重启MySQL服务 service mysql restartShutting down MySQL… SUCCESS! Starting MySQL… SUCCESS! 重启成功 2.登…...

push [特殊字符] present

push &#x1f19a; present 前言present和dismiss特点代码演示 push和pop特点代码演示 前言 在 iOS 开发中&#xff0c;push 和 present 是两种不同的视图控制器切换方式&#xff0c;它们有着显著的区别。 present和dismiss 特点 在当前控制器上方新建视图层级需要手动调用…...

RabbitMQ入门4.1.0版本(基于java、SpringBoot操作)

RabbitMQ 一、RabbitMQ概述 RabbitMQ RabbitMQ最初由LShift和CohesiveFT于2007年开发&#xff0c;后来由Pivotal Software Inc.&#xff08;现为VMware子公司&#xff09;接管。RabbitMQ 是一个开源的消息代理和队列服务器&#xff0c;用 Erlang 语言编写。广泛应用于各种分布…...

腾讯云V3签名

想要接入腾讯云的Api&#xff0c;必然先按其文档计算出所要求的签名。 之前也调用过腾讯云的接口&#xff0c;但总是卡在签名这一步&#xff0c;最后放弃选择SDK&#xff0c;这次终于自己代码实现。 可能腾讯云翻新了接口文档&#xff0c;现在阅读起来&#xff0c;清晰了很多&…...

uniapp 实现腾讯云IM群文件上传下载功能

UniApp 集成腾讯云IM实现群文件上传下载功能全攻略 一、功能背景与技术选型 在团队协作场景中&#xff0c;群文件共享是核心需求之一。本文将介绍如何基于腾讯云IMCOS&#xff0c;在uniapp中实现&#xff1a; 群内文件上传/下载文件元数据管理下载进度追踪跨平台文件预览 二…...