当前位置: 首页 > 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视频文件....

【大模型RAG】拍照搜题技术架构速览:三层管道、两级检索、兜底大模型

摘要 拍照搜题系统采用“三层管道&#xff08;多模态 OCR → 语义检索 → 答案渲染&#xff09;、两级检索&#xff08;倒排 BM25 向量 HNSW&#xff09;并以大语言模型兜底”的整体框架&#xff1a; 多模态 OCR 层 将题目图片经过超分、去噪、倾斜校正后&#xff0c;分别用…...

vscode里如何用git

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

工业安全零事故的智能守护者:一体化AI智能安防平台

前言&#xff1a; 通过AI视觉技术&#xff0c;为船厂提供全面的安全监控解决方案&#xff0c;涵盖交通违规检测、起重机轨道安全、非法入侵检测、盗窃防范、安全规范执行监控等多个方面&#xff0c;能够实现对应负责人反馈机制&#xff0c;并最终实现数据的统计报表。提升船厂…...

Redis相关知识总结(缓存雪崩,缓存穿透,缓存击穿,Redis实现分布式锁,如何保持数据库和缓存一致)

文章目录 1.什么是Redis&#xff1f;2.为什么要使用redis作为mysql的缓存&#xff1f;3.什么是缓存雪崩、缓存穿透、缓存击穿&#xff1f;3.1缓存雪崩3.1.1 大量缓存同时过期3.1.2 Redis宕机 3.2 缓存击穿3.3 缓存穿透3.4 总结 4. 数据库和缓存如何保持一致性5. Redis实现分布式…...

【决胜公务员考试】求职OMG——见面课测验1

2025最新版&#xff01;&#xff01;&#xff01;6.8截至答题&#xff0c;大家注意呀&#xff01; 博主码字不易点个关注吧,祝期末顺利~~ 1.单选题(2分) 下列说法错误的是:&#xff08; B &#xff09; A.选调生属于公务员系统 B.公务员属于事业编 C.选调生有基层锻炼的要求 D…...

ios苹果系统,js 滑动屏幕、锚定无效

现象&#xff1a;window.addEventListener监听touch无效&#xff0c;划不动屏幕&#xff0c;但是代码逻辑都有执行到。 scrollIntoView也无效。 原因&#xff1a;这是因为 iOS 的触摸事件处理机制和 touch-action: none 的设置有关。ios有太多得交互动作&#xff0c;从而会影响…...

大数据学习(132)-HIve数据分析

​​​​&#x1f34b;&#x1f34b;大数据学习&#x1f34b;&#x1f34b; &#x1f525;系列专栏&#xff1a; &#x1f451;哲学语录: 用力所能及&#xff0c;改变世界。 &#x1f496;如果觉得博主的文章还不错的话&#xff0c;请点赞&#x1f44d;收藏⭐️留言&#x1f4…...

优选算法第十二讲:队列 + 宽搜 优先级队列

优选算法第十二讲&#xff1a;队列 宽搜 && 优先级队列 1.N叉树的层序遍历2.二叉树的锯齿型层序遍历3.二叉树最大宽度4.在每个树行中找最大值5.优先级队列 -- 最后一块石头的重量6.数据流中的第K大元素7.前K个高频单词8.数据流的中位数 1.N叉树的层序遍历 2.二叉树的锯…...

Web后端基础(基础知识)

BS架构&#xff1a;Browser/Server&#xff0c;浏览器/服务器架构模式。客户端只需要浏览器&#xff0c;应用程序的逻辑和数据都存储在服务端。 优点&#xff1a;维护方便缺点&#xff1a;体验一般 CS架构&#xff1a;Client/Server&#xff0c;客户端/服务器架构模式。需要单独…...

SDU棋界精灵——硬件程序ESP32实现opus编码

一、 ​​音频处理框架​ 该项目基于Espressif的音频处理框架构建,核心组件包括 ESP-ADF 和 ESP-SR,以下是完整的音频处理框架实现细节: 1.核心组件 (1) 音频前端处理 (AFE - Audio Front-End) ​​main/components/audio_pipeline/afe_processor.c​​功能​​: 声学回声…...