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

推荐订阅源

T
Tailwind CSS Blog
大猫的无限游戏
大猫的无限游戏
L
LINUX DO - 热门话题
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
雷峰网
雷峰网
aimingoo的专栏
aimingoo的专栏
博客园_首页
MongoDB | Blog
MongoDB | Blog
V
V2EX
GbyAI
GbyAI
量子位
Microsoft Azure Blog
Microsoft Azure Blog
有赞技术团队
有赞技术团队
G
Google Developers Blog
云风的 BLOG
云风的 BLOG
B
Blog
Microsoft Security Blog
Microsoft Security Blog
S
SegmentFault 最新的问题
O
OpenAI News
N
News and Events Feed by Topic
博客园 - Franky
爱范儿
爱范儿
Forbes - Security
Forbes - Security
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
V2EX - 技术
V2EX - 技术
Application and Cybersecurity Blog
Application and Cybersecurity Blog
N
News and Events Feed by Topic
N
News | PayPal Newsroom
Schneier on Security
Schneier on Security
Cloudbric
Cloudbric
Security Archives - TechRepublic
Security Archives - TechRepublic
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
Recent Commits to openclaw:main
Recent Commits to openclaw:main
人人都是产品经理
人人都是产品经理
P
Privacy International News Feed
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
B
Blog RSS Feed
阮一峰的网络日志
阮一峰的网络日志
D
DataBreaches.Net
Last Week in AI
Last Week in AI
罗磊的独立博客
Spread Privacy
Spread Privacy
Recent Announcements
Recent Announcements
The Cloudflare Blog
Google DeepMind News
Google DeepMind News
AWS News Blog
AWS News Blog
The Register - Security
The Register - Security
Y
Y Combinator Blog
J
Java Code Geeks
I
Intezer

元视角

.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 扩展之属性注入实现 - 元视角
教你一步步实现一个虚拟摇杆 - 元视角
飞鸿踏雪 · 2015-10-30 · via 元视角

各位朋友,大家好,我是秦元培,欢迎大家关注我的博客,我的博客地址是http://qinyuanpei.com。最近因为项目需要决定尝试自己来实现一个虚拟摇杆,所以在今天的文章中我们的目标是使用 uGUI 来制作一个可以在移动平台稳定运行的虚拟摇杆(请不要问我为什么不使用 NGUI 来实现,你说我做个虚拟摇杆有必要在项目里导入那么多的资源嘛 23333)。关于使用第三方插件来实现虚拟摇杆,请大家参照我以前写的文章Unity3D 游戏开发之使用 EasyTouch 虚拟摇杆控制人物移动,在这里就不再赘述了。

虚拟摇杆这种输入方式相信大家在手机游戏平台上已经相当的熟悉了,首先我们来简单了解下虚拟摇杆的设计原理。虚拟摇杆有一张固定的 2D 贴图(背景层)和一张可拖动的 2D 贴图(控制层)构成,默认情况下控制层在背景层的中心,我们称这个位置为初始位置,当移动控制层后移动层的位置会发生变化,此时控制层的当前位置和初始位置两点间可以计算出一个 2D 向量,通过这个向量我们就可以判断虚拟摇杆的移动方向。在经典的八方向摇杆导航中摇杆中可移动方向被分成了上、左上、右上、下、左下、右下、左、右共 8 个方向。我们知道根据三角函数可以非常容易地计算出这个 2D 向量的角度并由此判定摇杆是在向着这 8 个方向中的哪一个方向移动。在今天的文章中,我们不需要考虑这 8 个方向,因为我们可以向任何一个方向进行移动。

好了,首先在场景中创建两个 Image 组件和一个空的游戏体,然后将这两个 Image 组件拖拽到这个空的游戏体下使它们称为其子节点。这里需要注意的是这两个 Image 的层级关系。现在我们来编写脚本,这个脚本将被添加到控制层物体上:


/*
 * uGUI虚拟摇杆
 * 作者:秦元培
 * 博客:http://qinyuanpei.com
 * 时间:2015年10月24日
 */

using UnityEngine;
using System.Collections;
using UnityEngine.UI;
using UnityEngine.EventSystems;

public class JoyStick : MonoBehaviour,IPointerDownHandler, IPointerUpHandler, IDragHandler
{

    /// <summary>
    /// 摇杆最大半径
    /// 以像素为单位
    /// </summary>
    public float JoyStickRadius = 50;

    /// <summary>
    /// 摇杆重置所诉
    /// </summary>
    public float JoyStickResetSpeed = 5.0f;

    /// <summary>
    /// 当前物体的Transform组件
    /// </summary>
    private RectTransform selfTransform;

    /// <summary>
    /// 是否触摸了虚拟摇杆
    /// </summary>
    private bool isTouched = false;

    /// <summary>
    /// 虚拟摇杆的默认位置
    /// </summary>
    private Vector2 originPosition;

    /// <summary>
    /// 虚拟摇杆的移动方向
    /// </summary>
    private Vector2 touchedAxis;
    public Vector2 TouchedAxis
    {
        get
        {
            if(touchedAxis.magnitude < JoyStickRadius)
                return touchedAxis.normalized / JoyStickRadius;
            return touchedAxis.normalized;
        }
    }

	/// <summary>
	/// 定义触摸开始事件委托 
	/// </summary>
	public delegate void JoyStickTouchBegin(Vector2 vec);

    /// <summary>
    /// 定义触摸过程事件委托 
    /// </summary>
    /// <param name="vec">虚拟摇杆的移动方向</param>
	public delegate void JoyStickTouchMove(Vector2 vec);

    /// <summary>
    /// 定义触摸结束事件委托
    /// </summary>
	public delegate void JoyStickTouchEnd();

    /// <summary>
    /// 注册触摸开始事件
    /// </summary>
	public event JoyStickTouchBegin OnJoyStickTouchBegin;

    /// <summary>
    /// 注册触摸过程事件
    /// </summary>
	public event JoyStickTouchMove OnJoyStickTouchMove;

    /// <summary>
    /// 注册触摸结束事件
    /// </summary>
	public event JoyStickTouchEnd OnJoyStickTouchEnd;

	void Start () 
    {
        //初始化虚拟摇杆的默认方向
        selfTransform = this.GetComponent<RectTransform>();
        originPosition = selfTransform.anchoredPosition;
	}


    public void OnPointerDown(PointerEventData eventData)
    {
        isTouched = true;
		touchedAxis = GetJoyStickAxis(eventData);
		if(this.OnJoyStickTouchBegin != null)
			this.OnJoyStickTouchBegin(TouchedAxis);
    }

    public void OnPointerUp(PointerEventData eventData)
    {
        isTouched = false;
		selfTransform.anchoredPosition = originPosition;
		touchedAxis = Vector2.zero;
		if(this.OnJoyStickTouchEnd != null)
			this.OnJoyStickTouchEnd();
    }

    public void OnDrag(PointerEventData eventData)
    {
		touchedAxis = GetJoyStickAxis(eventData);
		if(this.OnJoyStickTouchMove != null)
			this.OnJoyStickTouchMove(TouchedAxis);
    }


    void Update()
    {
        //当虚拟摇杆移动到最大半径时摇杆无法拖动
        //为了确保被控制物体可以继续移动
        //在这里手动触发OnJoyStickTouchMove事件
        if(isTouched && touchedAxis.magnitude>=JoyStickRadius)
        {
            if(this.OnJoyStickTouchMove != null)
                this.OnJoyStickTouchMove(TouchedAxis);
        }

        //松开虚拟摇杆后让虚拟摇杆回到默认位置
        if(selfTransform.anchoredPosition.magnitude > originPosition.magnitude)
            selfTransform.anchoredPosition -= TouchedAxis * Time.deltaTime * JoyStickResetSpeed;
    }

	/// <summary>
	/// 返回虚拟摇杆的偏移量
	/// </summary>
	/// <returns>The joy stick axis.</returns>
	/// <param name="eventData">Event data.</param>
	private Vector2 GetJoyStickAxis(PointerEventData eventData)
	{
		//获取手指位置的世界坐标
		Vector3 worldPosition;
		if (RectTransformUtility.ScreenPointToWorldPointInRectangle (selfTransform,
		         eventData.position, eventData.pressEventCamera, out worldPosition))
			selfTransform.position = worldPosition;
		//获取摇杆的偏移量
		Vector2 touchAxis = selfTransform.anchoredPosition-originPosition;
		//摇杆偏移量限制
		if(touchAxis.magnitude >= JoyStickRadius) 
		{
			touchAxis = touchAxis.normalized * JoyStickRadius;
			selfTransform.anchoredPosition = touchAxis;
		}
		return touchAxis;
	}


}

在这段脚本中,我们实现了 OnPointerDown、OnPointerUp 和 OnDrag 三个 uGUI 事件接口,然后注册了相关的事件委托,这里借鉴了 EasyTouch 的设计,可以使得虚拟摇杆的逻辑和角色控制逻辑相互分离。这里的核心方法是 GetJoyStickAxis()方法,通过这个方法我们可以获得一个 Vector2 类型的值,它表示的是未标准化过的虚拟摇杆的偏移量。这里的 RectTransformUtility.ScreenPointToWorldPointInRectangle()方法表示将一个屏幕坐标转化为对应 RectTransform 的世界坐标,RectTransform 的 anchoredPosition 属性表示的是当前元素在场景中的屏幕坐标。我们知道屏幕坐标是以像素为单位的,因此这里使用屏幕坐标可以计算出虚拟摇杆在水平方向和垂直方向上移动了多少个像素,我们以此来作为虚拟摇杆的偏移量衡量指标。TouchedAxis 是经过标准化以后的偏移量,我们将把这个值传递到事件委托中以提供给外部来调用。好了,要说的就这些了,没有说到的大家可以看看代码里的注释或者是在博客中给我留言,就是这样啦。

接下来,我们在场景中添加一个角色模型来测试我们编写的虚拟摇杆,因为在 JoyStick 中我们已经定义了事件委托,所以在这里就是简单的调用啦。好了,我们一起来看看代码吧!


/*
 * Joystick3D.cs
 * 3D模式下的虚拟摇杆测试
 * 作者:秦元培
 * 博客:http://qinyuanpei.com
 * 时间:2015年10月30日
 */

using UnityEngine;
using System.Collections;

public class JoyStick3D : MonoBehaviour 
{

	private JoyStick js;
	
	void Start () 
	{
		js = GameObject.FindObjectOfType<JoyStick> ();
		js.OnJoyStickTouchBegin += OnJoyStickBegin;
		js.OnJoyStickTouchMove += OnJoyStickMove;
		js.OnJoyStickTouchEnd += OnJoyStickEnd;
	}
	

	void OnJoyStickBegin(Vector2 vec)
	{
        Debug.Log("开始触摸虚拟摇杆");
	}

	void OnJoyStickMove (Vector2 vec)
	{
        Debug.Log("正在移动虚拟摇杆");

        //设置角色朝向
		Quaternion q = Quaternion.LookRotation (new Vector3 (vec.x, 0, vec.y));
		transform.rotation = q;

        //移动角色并播放奔跑动画
		transform.Translate(Vector3.forward * 75f * Time.deltaTime);
		animation.CrossFade("Run");
	}
	
	void OnJoyStickEnd ()
	{
        Debug.Log("触摸移动摇杆结束");

        //播放默认待机动画
		animation.CrossFade("idle");
	}

    void OnGUI()
    {
        GUI.Label(new Rect(30,30,200,30),"3D模式下的虚拟摇杆测试");
    }
}

最终程序的运行效果如下图所示,我们编写的这个虚拟摇杆可以在手机上完美的运行,欢饮大家来一起测试和吐槽!

2D模式演示 2D模式演示

3D模式演示 3D模式演示

好了,今天的内容就是这样啦!欢迎大家继续关注我的博客,希望大家喜欢!