惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

C
CXSECURITY Database RSS Feed - CXSecurity.com
V2EX - 技术
V2EX - 技术
美团技术团队
NISL@THU
NISL@THU
C
CERT Recently Published Vulnerability Notes
Google DeepMind News
Google DeepMind News
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
MyScale Blog
MyScale Blog
C
Cybersecurity and Infrastructure Security Agency CISA
W
WeLiveSecurity
博客园 - 聂微东
Hacker News - Newest:
Hacker News - Newest: "LLM"
H
Help Net Security
GbyAI
GbyAI
G
GRAHAM CLULEY
The Last Watchdog
The Last Watchdog
U
Unit 42
罗磊的独立博客
B
Blog RSS Feed
K
Kaspersky official blog
宝玉的分享
宝玉的分享
Blog — PlanetScale
Blog — PlanetScale
Know Your Adversary
Know Your Adversary
IT之家
IT之家
www.infosecurity-magazine.com
www.infosecurity-magazine.com
Y
Y Combinator Blog
S
Secure Thoughts
P
Privacy & Cybersecurity Law Blog
S
Schneier on Security
Last Week in AI
Last Week in AI
Hacker News: Ask HN
Hacker News: Ask HN
L
LINUX DO - 热门话题
雷峰网
雷峰网
Martin Fowler
Martin Fowler
Recent Commits to openclaw:main
Recent Commits to openclaw:main
N
Netflix TechBlog - Medium
T
Tor Project blog
The GitHub Blog
The GitHub Blog
The Hacker News
The Hacker News
F
Fortinet All Blogs
Webroot Blog
Webroot Blog
Spread Privacy
Spread Privacy
Cloudbric
Cloudbric
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
Schneier on Security
Schneier on Security
人人都是产品经理
人人都是产品经理
P
Privacy International News Feed
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
T
The Exploit Database - CXSecurity.com
Microsoft Security Blog
Microsoft Security Blog

元视角

.NET 生态下的 Agent 框架选型:从 ReAct 到原生推理 - 元视角 从「能用」到「好用」:LLM 流式响应实现方式的探索之路 - 元视角 当我用 2000 条聊天记录,让 AI 为我画一幅自画像 - 元视角 基于 Supabase 的 AI 应用开发探索 - 元视角 微博 × MCP:社交媒体新玩法解锁 - 元视角 四点钟海棠花未眠 - 元视角 Semantic Kernel × MCP:智能体的上下文增强探索 - 元视角 基于 K-Means 聚类分析实现人脸照片的快速分类 - 元视角 容器技术驱动下的代码沙箱实践与思考 - 元视角 温故而知新:后端通用查询方案的再思考 - 元视角 浅议 CancellationToken 在前后端协同取消场景中的应用 - 元视角 Semantic Kernel 视角下的 Text2SQL 实践与思考 - 元视角 关于 ChatGPT 的流式传输,你需要知道的一切 - 元视角 RAG 的是与非、Rewrite 和 Rerank - 元视角 使用 EFCore 和 PostgreSQL 实现向量存储及检索 - 元视角 基于 LLaMA 和 LangChain 实践本地 AI 知识库 - 元视角 使用 llama.cpp 在本地部署 AI 大模型的一次尝试 - 元视角 如何为 Git 配置多个 SSH Key - 元视角 C# 使用 LibUsbDotNet 实现 USB 设备检测 - 元视角 基于 C# 实现样式与数据分离的打印方案 - 元视角 基于 SVG 的图形交互方案实践 - 元视角 前端视频播放技术概览 - 元视角 温故而知新,再话 Python 动态导入 - 元视角 后 GPT 时代,NLP 不存在了? - 元视角 视频是不能 P 的系列:使用 Milvus 实现海量人脸快速检索 - 元视角 GDI+下字体大小自适应方案初探 - 元视角 小爱音箱集成 ChatGPT 的不完全教程 - 元视角 程序员视角下的三体世界随想 - 元视角 关于 Docker 容器配置信息的渐进式思考 - 元视角 在 Docker 容器内集成 Crontab 定时任务 - 元视角 为你的服务器集成 LDAP 认证 - 元视角 似花还似非花 - 元视角 视频是不能 P 的系列:使用 Dlib 实现人脸识别 - 元视角 浅议分布式链路追踪与日志的整合 - 元视角 关于 Git 大文件上传这件小事 - 元视角 .NET 进程内队列 Channel 的入门与应用 - 元视角 使用 Fody 实现 .NET 的静态编织 - 元视角 .NET Core + ELK 搭建可视化日志分析平台(下) - 元视角 聊一聊前端图片懒加载背后的故事 - 元视角 支持外部链接跳转的 Vue Router 扩展实现 - 元视角 视频是不能 P 的系列:OpenCV 和 Dlib 实现表情包 - 元视角 不得不说的 ASP.NET Core 集成测试 - 元视角 再议 DDD 视角下的 EFCore 与 领域事件 - 元视角 Vue.js 前端项目容器化部署实践极简教程 - 元视角 再见,人间四月天 - 元视角 Python 图像风格化迁移助力画家梦想 - 元视角 利用 ASP.NET Core 中的标头传播实现分布式链路追踪 - 元视角 利用 gRPC 实现文件的上传与下载 - 元视角 七种武器:延迟队列的原理和实现总结 - 元视角 gRPC 流式传输极简入门指南 - 元视角 Envoy 集成 Jaeger 实现分布式链路追踪 - 元视角 浅议非典型 Web 应用场景下的身份认证 - 元视角 gRPC 借助 Any 类型实现接口的泛化调用 - 元视角 分布式丛林探险系列之 Redis 集群模式 - 元视角 分布式丛林探险系列之 Redis 主从复制模式 - 元视角 通过 Python 预测 2021 年双十一交易额 - 元视角 gRPC 搭配 Swagger 实现微服务文档化 - 元视角 SSL/TLS 加密传输与数字证书的前世今生 - 元视角 使用 Python 自动识别防疫健康码 - 元视角 你不可不知的容器编排进阶技巧 - 元视角 ASP.NET Core 搭载 Envoy 实现 gRPC 服务代理 - 元视角 再话 AOP,从简化缓存操作说起 - 元视角 ASP.NET Core 搭载 Envoy 实现微服务身份认证(JWT) - 元视角 ASP.NET Core 搭载 Envoy 实现微服务的监控预警 - 元视角 ASP.NET Core 搭载 Envoy 实现微服务的反向代理 - 元视角 ASP.NET Core gRPC 打通前端世界的尝试 - 元视角 EFCore 实体命名约定库:EFCore.NamingConventions - 元视角 ASP.NET Core gRPC 集成 Polly 实现优雅重试 - 元视角 ASP.NET Core gRPC 健康检查的探索与实现 - 元视角 ASP.NET Core gRPC 拦截器的使用技巧分享 - 元视角 SnowNLP 使用自定义语料进行模型训练 - 元视角 使用 HttpMessageHandler 实现 HttpClient 请求管道自定义 - 元视角 ABP vNext 的实体与服务扩展技巧分享 - 元视角 ABP vNext 对接 Ant Design Vue 实现分页查询 - 元视角 源代码探案系列之 .NET Core 跨域中间件 CORS - 元视角 源代码探案系列之 .NET Core 限流中间件 AspNetCoreRateLimit - 元视角 源代码探案系列之 .NET Core 并发限制中间件 ConcurrencyLimiter - 元视角 通过 EmbededFileProvider 实现 Blazor 的静态文件访问 - 元视角 低代码,想说爱你不容易 - 元视角 记一次失败的 ThoughtWorks 面试经历 - 元视角 从 C# 1.0 到 C# 9.0,历代 C# 语言特性一览 - 元视角 通过 Python 分析 2020 年全年微博热搜数据 - 元视角 基于 Python 和 Selenium 实现 CSDN 一键三连自动化 - 元视角 使用多线程为你的 Python 爬虫提速的 N 种姿势,你会几种? - 元视角 实现网页长截图的常见思路总结 - 元视角 温故而知新,由 ADO.NET 与 Dapper 所联想到的 - 元视角 视频是不能 P 的系列:OpenCV 人脸检测 - 元视角 作为技术宅的我,是这样追鬼滅の刃的 - 元视角 使用 Python 抽取《半泽直树》原著小说人物关系 - 元视角 厉害了!打工人用 Python 分析西安市职位信息 - 元视角 使用 dotTrace 对 .NET 应用进行性能分析与优化 - 元视角 一道 HashSet 面试题引发的蝴蝶效应 - 元视角 基于选项模式实现.NET Core 的配置热更新 - 元视角 Dapper.Contrib 在 Oracle 环境下引发 ORA-00928 异常问题的解决 - 元视角 .NET Core 中对象池(Object Pool)的使用 - 元视角 利用 MySQL 的 Binlog 实现数据同步与订阅(下):EventBus 篇 - 元视角 利用 MySQL 的 Binlog 实现数据同步与订阅(中):RabbitMQ 篇 - 元视角 利用 MySQL 的 Binlog 实现数据同步与订阅(上):基础篇 - 元视角 记一次从已损坏的 Git 仓库中找回代码的经历 - 元视角 .NET Core 原生 DI 扩展之属性注入实现 - 元视角
在 Unity3D 中使用 uGUI 实现 3D 旋转特效 - 元视角
飞鸿踏雪 · 2016-07-10 · via 元视角

各位朋友大家好,欢迎大家关注我的博客,我是 Payne,我的博客地址是http://qinyuanpei.com。最近一位朋友问我,如何在 Unity 引擎中实现类似《英雄联盟》中选择皮肤时的 3D 滚动视图效果,虽然我非常不喜欢这个游戏,可是大学四年在宿舍里被周围同学们耳濡目染,对这个游戏中常见英雄的口头禅还是颇为熟悉的,曾经在周围同学的“硝烟”和“噪杂”中熬夜编程,此时此刻想起来大概是最能让我怀念和骄傲的记忆了。剑圣说“你的剑就是我的剑”,伊泽瑞尔说“是时候表演真正的技术了”,杰斯说“为了更美好的明天而战”……或许曾经的某一瞬间,我们曾经有过类似的让你我疯狂着迷的人生信条,可是不管怎样,我希望我们可以将这些永远地铭刻在心里,如同心中栽种下一棵红莲,在黑夜中静静地等待开放,这样当此去经年亦或时过境迁的时候,我们不会说是时光抹去了你我年轻的棱角,因为我相信真正的棱角会因为磨砺而变得更加明亮,绝对不会因为此刻的苟且就变的麻木甚至迷茫。好了,喝完我这碗心灵鸡汤,下面我们来一起学习如何在 Unity3D 中使用 uGUI 实现 3D 滚动视图效果。

需求分析

首先,我们先来对这个需求进行分析,从这篇文章的题目我们获得的一个关键信息是,希望通过某种方式实现 3D 滚动特效。因此我们首先要解决的一个问题是,我们应该采用 2D 方式来实现还是采用 3D 方式来实现这种界面效果。我们假定这里希望实现的效果如下图所示,我们可以注意到从这张图片的设计初衷来看,它更像是一种介绍产品特性的文案设计,我们这里仅仅是想通过这张图告诉大家,我们需要实现一个什么样的效果。软件开发过程中最大的成本在我看来主要来自沟通。因为事实上对普通用户而言技术并不重要,重要的是能否实现用户想要的功能,可是大部分情形是用户并不知道自己想要什么,除非你将实际的产品放到用户眼前甚至手中。好了,在对需求有了一个基本的印象以后,我们来思考如何实现这个需求。

具体来讲,我们有两种思路:

  • 其一是采用真实的 3D 来制作,即我们通过一个圆柱体或者是多棱柱将图片"粘贴"在不同的面上,通过对圆柱体或者多棱柱进行旋转,然后以真实的 3D 的形式来呈现给用户。
  • 其二是采用伪 3D 来制作,即我们通过在 2D 平面内对图片的层次进行合理化调整实现伪 3D 效果,配合插值、缩放等技巧来实现 2D 平面上的旋转,然后给用户一种视觉上的 3D 效果。

需求设定 需求设定

核心原理

在这里我们选择采用伪 3D 来制作,为什么选择这种方案呢?因为它简单啊,哈哈。好了,我们现在将实际的需求进行抽象,我们会发现什么呢?我们注意到这本质上是一个曲线问题,我们可以将每个图片的中心用平滑的曲线连接起来,然后我们就得到了一条抛物线或者是圆锥曲线或者是贝塞尔曲线,在这里我们将其理解为什么样的曲线并不重要,因为这最终影响到的是曲线的平滑度问题,即细节上的调整。沿着这个思路,我们就意识到,这是一个根据曲线平均分布坐标点的过程,假设我们这里 5 张图片,并且曲线在中间位置可以找到一条垂直的对称轴,那么我们只需要将这 5 个点在水平方向上平均分布即可,事实上根据人类视觉的特点,这个距离应该是越来越小的,就像我们看到的一排并列的树木,越远的地方它们的间距会越来越小,而事实上它们的间距是一样的,根据这个特性我们可以表现出这种视觉上的纵深的感觉,在实际项目中它取决于美术设定和策划设定,我们这里就从最简单的情况开始分析。

好了,在解决了精灵放置的这个问题以后,我们接下来要解决的是什么呢?答案是精灵的层级,因为层级能够帮助我们营造一种视觉上的层次感和立体感,比如在跑酷游戏中我们常常使用视差滚动这种技术来表现 3D 效果,以及传统的斜 45 度瓦片地图来实现 2.5D 效果都是使用 2D 来模拟 3D 效果的经典案例。所以在这里除了确定每个精灵的放置位置以外,我们还有一个问题,如何对这些精灵进行排序,所幸的是在 uGUI 中我们可以通过 SetSiblingIndex 方法来设置一个精灵的深度,当每次通过按钮切换精灵的时候,我们都需要对所有精灵重新计算坐标和深度,而为了更好的视觉表现力,我们可以在切换的时候做一个简单的位移动画,至此我们就可以开始动手实现功能啦。

具体实现

首先我们来搭建一个基本的场景,我们这里将一切浮华褪尽,我们可以看到在场景中有两个按钮,它们可以让我们当前选中的卡片,而界面底部的标签会显示我们当前选择的角色名称。虽然在这里采用触屏滑动的效果更好,可我们这里主要的目的是为了说明如何实现我们的思路,当引入这部分功能的设计以后,会增加大家在整体理解上的难度,所以我们这里以快速实现功能为主。注意到场景中的卡片此时都是相当“任性”地放置在界面上,这是因为我们稍后会采用算法计算每个卡片的实际位置,所以在这里完全可以忽略其“美观性”。

场景展示 场景展示

这里,我们设定场景的大小为 800x460,那么在这种情况下,我们可以按照下面图中所示的曲线轨迹来构造一条曲线,考虑到椭圆方程比贝塞尔曲线更加简单易用,所以我们这里选择椭圆方程来作为场景中这些卡片排列的曲线方程。

曲线方程 曲线方程

此时以屏幕中心为原点构建平面直角坐标系,则这个椭圆是一个以长轴 2A=400、短轴 2B=640、中心在(0,320)上的椭圆。根据这个原理,我们可以将其代码实现分为三个步骤来实现。首先,我们将场景中的所有卡片存储在 GameObject 数组中,这里我们这里规定卡片的数目必须为奇数,然后我们从左到右依次计算每个卡片的位置和深度,这样就可以让卡片按照我们期望的方式进行排列啦。下面一起来看代码如何实现:

//初始化精灵数组
int childCount = transform.childCount;
//计算两侧精灵数目
halfSize = (childCount-1) / 2;
//初始化精灵
sprites = new GameObject[childCount];
for(int i = 0; i < childCount; i++)
{
   sprites[i] = transform.GetChild(i).gameObject;
   SetPosition(i);
   SetDeepin(i);
}

这里 sprites 显然是一个 GameObject[],因为卡片的数目为奇数个,所以 halfSize 是指中间位置卡片的索引,这里需要两个辅助方法,SetPosition 和 SetDeepin,从名字我们就知道这两个方法分别是设置卡片位置和设置卡片深度。当我们提到代码注释的时候,好多人以代码自注释为理解逃避注释,孰不知这建立在命名规范的基础上,如果你连这点基本的要求都做不到,我建议你还是多写点注释、少写点代码。好了,这两个方法的实现细节如下:

/// <summary>
/// 设置精灵位置
/// </summary>
private void SetPosition(int index)
{
   //计算第index个精灵的角度
   float angle = 0.0f;
   if (index < halfSize) {
      angle = startAngle - (halfSize - index) * DeltaAngle;
   } else if (index > halfSize) {
      angle = startAngle + (index - halfSize) * DeltaAngle;
   } else {
      angle = startAngle;
   }

   //计算第index个精灵的坐标
   float x = A* Mathf.Cos((angle/180) * Mathf.PI) + Center.x;
   float y = B* Mathf.Sin((angle/180) * Mathf.PI) + Center.y;
   Vector3 v3 = Camera.main.WorldToScreenPoint(new Vector3(x,y,0));
   v3 = Camera.main.ScreenToWorldPoint(v3);
   Vector2 v2 = new Vector2(v3.x,v3.y);
   sprites[index].GetComponent<RectTransform>().anchoredPosition = v2;
}

可以注意到,在这里我们根据精灵索引 index 和两侧精灵数目 halfSize 的关系,按照 DeltaAngle 这个增量来计算每个精灵实际的角度,在此基础上结合椭圆的参数方程,我们可以非常容易地计算出每个精灵实际的位置,这样就可以保证精灵中心都在椭圆曲线上。好了,接下来我们会遇到一个新的问题,这些精灵的层级应该是从中间位置向两边依次递减的,所以为了解决这个问题,我们还需要对每个精灵的层级进行计算,这部分代码的实现细节如下:

/// <summary>
/// 设置精灵深度
/// </summary>
private void SetDeepin(int index)
{
   //计算精灵深度
   int deepin = 0;
   if(index < halfSize) {
      deepin = index;
   } else if(index > halfSize) {
      deepin = sprites.Length - (1 + index);
   } else{
      deepin = halfSize;
   }
   sprites[index].GetComponent<RectTransform>().SetSiblingIndex(deepin);
}

事实上,我在这里并不清楚 SetSiblingIndex 这个方法的真正作用:),可是它的确能够实现我们想要的功能。有时候在维护一个古老的项目的时候,可能你会在代码中看到各种有趣的注释,而这些注释中有相当一些都充满了一种“形而上学”的味道在里面,我们不知道这个世界为什么会是这样,可是看起来它们都运行地非常良好。或许这就是这个世界的奇妙之处,无论我们是否想要尝试打破这些规则,这个世界上总是有些我们难以理解的东西存在,可是存在即合理,不是吗?理性思维的缺陷在于想要为一切问题找到一个答案,所以这次苏格拉没有底,我们就感性一次又何妨呢,这个问题就让它没有答案吧!

现在,显然我们需要解决一个新的问题,就像上帝在我们关上一扇门的同时,会为我们开启一扇窗口。理论上任何问题都可以通过引入一个中间层来解决,而引入中间层的同时毫无疑问地引入了一个新的问题。在这里我们已经完成了让所有精灵按照椭圆曲线进行排布以及精灵的层级关系这两个问题,可是我们这是一个静态的过程啊,我们需要的是让它能够滚动起来,所以怎么解决这个问题呢?我们可以注意到的一点是,精灵的这种“滚动”效果,实际上是将数组中的第一个元素 sprites[0]或者最后一个元素 sprites[sprites.Length-1],依次和数组中的第 i 个元素进行交换。比如精灵整体向右侧“滚动”,我们只需要从第一个元素开始依次和最后一个元素进行交换就可以啦,所以这里的实现实际上是:

/// <summary>
/// 向后翻页
/// </summary>
public void OnNext()
{
   int length = sprites.Length;
   for (int i = 0; i < length; i++)
   {
      GameObject temp = sprites[i];
      sprites[i] = sprites[length - 1];
      sprites[length-1] = temp;
   }

   for (int i = 0; i < length; i++)
   {
      SetPosition(i);
      SetDeepin(i);
   }
}

我们在对数组内的元素重新组织后,需要重新计算每个精灵的位置和深度。我这里在思考的一个问题是:精灵的位置和深度实际上是确定的,所以我们可以考虑将它们存储起来“复用”,这样可以减少每次的重复计算。其实,代码的优化和重构是一个需要时间来酝酿的过程,没有人能够在写代码的时候,就可以意识到代码中的瑕疵,而这种发现问题的眼光通常需要长时间的培养,这是我们之所以提倡不要过早优化的原因,除非你能够快速地找到代码中的优化点。好了,现在采用类似的思路,我们可以实现向前翻页的逻辑啦,这里的代码非常简单不再赘述。

好了,现在我们可以看看到目前为止我们实现了一个怎样的功能吧!

效果展示 效果展示

其实这篇文章我还想继续再往下写的,可是因为我比较懒一直拖着不写,以及接下来相当多的内容都是和界面相关的东西,所以我决定这篇文章就暂时写到这里,目前这个方案可以实现一个简单的“3D”滚动的效果,按照这个思路,接下来我们要做的事情是让滚动更加平滑以及支持鼠标或者触屏操作,毕竟这个需求的出发点是来自一个游戏,所以我们可以考虑在“滚动”的时候增加插值特性,与此同时,为了让它更加具有“3D”的感觉,可以在设置精灵层级的时候为不同的精灵设置不同的缩放比例,这样会更加符合美术中的透视关系,效果应该会更好吧!我认识的一位朋友使用 uGUI 中原生控件 ScrollRect 实现了类似的功能,感觉她还是非常厉害的啊,果然我不再从事 Unity 开发以后,我在这块的技术完全跟不上整个技术圈的节奏啊。

小结

本文介绍了一种基于曲线方程来构建伪 3D 效果的思路,主要借助椭圆的参数方程来计算精灵位置,使其实现按照椭圆曲线进行排布的效果,在此基础上配合层级调整、插值、缩放等技巧,在一定程度上可以实现 2D 平面内的伪 3D 旋转效果。因为博主身患拖延症晚期,所以这篇文章在拖延了很久以后,终于成功的成为了一个没有填完的坑,不过我相信掌握原理比获取代码更为重要,所以这篇文章更多的是希望能给大家提供相关思路,博主在这篇文章中没有实现的功能,各位读者有兴趣的话可以考虑自行实现,写完这篇文章表示心好累,好了,就这样吧,各位晚安!