【Godot4.2】基础知识 - Godot中的2D向量

news2024/11/18 23:34:40

概述

在Godot中,乃至一切游戏编程中,你应该都躲不开向量。这是每一个初学者都应该知道和掌握的内容,否则你将很难理解和实现某些其实原理非常简单的东西。

估计很多刚入坑Godot的小伙伴和我一样,不一定是计算机专业或编程相关专业从业人员。英语、数学、算法、设计模式以及Shader方面都是拦路虎。尤其数学,当初稀里糊涂,现在也早还给老师了。我本人就是个数学学渣,所以也是一路学引擎,一路补课数学、英语和编程知识。

本篇尽量由浅入深,让新手们不再像我当初初学时那样迷茫。


说明:本文写于2022年11月26日,基于的Godot版本是3.5,与目前Godot4.2在某些语法和API上可能会有差异,后续会基于4.2进行改写和拓展。


概念

二维向量,是指有两个分量的向量。在Godot的内置脚本语言GDScript中,用Vector2D类型表示二维向量。

万能的二维向量

二维向量可以表示屏幕二维坐标系上的点的位置。可以表示方向,还可以存储矩形的尺寸。

也可以用一堆二维向量表示平面上的一条折线路径。

Node2D的朝向

在这里插入图片描述

每一个2D物体其实有两个方向

  • 第一个是它自身的朝向,也就是由它的rotation_degree属性所定义的方向
  • 第二个是在移动过程中,从自己的位置到目标位置的方向,也就是移动方向

不能只知道移动的方向,却忽略物体自身的朝向。在实际的设计中两者配合,才能做出更好的效果。

在Godot中确实没有“朝向”这个概念,只有rotation_degree属性,但是却的的确确向我们展示了“朝向”的存在。look_at()方法和经典的“Sprite随鼠标定位旋转”示例就表明了这一点。

Sprite随鼠标定位旋转

创建如下的场景,将icon.png拖进来,放到视口矩形范围的中间位置。
在这里插入图片描述

Icon节点添加如下代码:

extends Sprite


func _process(delta):
	look_at(get_global_mouse_position())

运行后就可以看到一个始终朝向鼠标位置的效果。而这里可以看到,始终是右边朝向鼠标位置。

Sprite节点始终“看”向鼠标位置

如果不够直观,我们可以给Sprite节点添加一个箭头,与其“朝向”保持一致。
image.png
你就可以看到更清晰的效果。
用红色箭头指示Sprite节点的朝向
如果看到上面的示例,你能够想到经典游戏《祖玛》,说明你很聪明。

经典游戏《祖玛》中随鼠标定位旋转的效果

这里我们不做《祖玛》的示例,换为两张坦克素材图片,组成一个坦克。

在这里插入图片描述

我们将之前的代码放到坦克的“炮身”也就是cannon节点上。

你立马就得到了一个可以随鼠标位置瞄准的坦克。

坦克随鼠标定位瞄准

向量与位置

屏幕坐标系

在二维平面上表示一个点,最简单的方式就是定义一个平面坐标系,然后用一对(x,y)坐标来表示。Godot采用的坐标系与大多数计算机可视化编程所采用的是一致的,也就是以左上角为原点(0,0),X轴正方向向右,Y轴正方向向下的“屏幕坐标系”。
Godot的屏幕坐标系
在Godot的2D工作区视口中,你可以看到Y轴用绿色,X轴用紫色,左上角原点也会有特殊的标记。

实际视口中的XY轴、原点和视口可视区域矩形

视口矩形

游戏窗口的尺寸是有限的,但是游戏地图或者游戏的世界可以比游戏窗口大得多。我们运行场景或项目时,窗口首先展示的屏幕范围,我们可以将其称为“第一屏”,这和网页设计中的“第一屏”概念是类似的。

“第一屏”是一个矩形区域,你可以用get_tree().root.get_visible_rect()获得包含其信息的Rect2数据。它与我们在项目设置中设置的窗口大小可能一致,也可能不一致,因为游戏窗口可以在运行过程中改变大小,并且还受缩放模式等设置的影响。

项目设置中的窗口大小

游戏中,我们通常不会始终待在“第一屏”,除非你的设计就是每个关卡地图就是一屏的大小。通过摄像机我们可以看到关卡地图的其他地方。

位置

好了,讲了必须讲的前置知识后,回到正题——“向量与位置”。在平面坐标系中,表示一个点的位置,就是用它在X轴和Y轴上的投影处的值组成的坐标。

点的坐标表示
那这又怎么跟向量扯上关系了呢?

向量其实可以理解为“相对移动”或“相对位置”,这种“相对性”其实很让人迷糊。但是举个例子你就明白了:

已知我们在地球上,已经通过地磁场确定了东西南北(废话),假设你的家乡在某个小镇A,但是你大学毕业后到了某个省的省会城市B工作,那么描述你的家乡小镇A和你工作的城市B之间的相对位置关系,你会怎么描述呢?

image.png
你会说你的家乡小镇A在你工作所在城市B的西北方向1200公里(数据胡诌),而你工作的城市B在你老家小镇A的东南方向1200公里公里。

你会发现两个地点的相对位置,可以用一个“方向+距离”的形式轻松的描述清楚。而“方向+距离”就是向量

在相对中引入绝对

世上本没有东西南北,人类用某种科学规律和约定俗成规定了东西南北的方向,地球也本没有经纬,同样是人类用某种科学规律和科学家之间的约定俗成创建了一个可以定位地球某个点的方法。

同样的在二维平面上,本没有坐标系,为了方便描述位置,才引入了平面坐标系。类比《圣经·创世纪》中:世上本没有光,“神说“要有光”,就有了光”,科学家们何尝不是分开混沌,理清世界的“神”。

在屏幕坐标系中,左上角是坐标系原点(0,0),那么屏幕上的任何一点都可以被理解为“基于坐标系原点(0,0)在某个方向上偏移多少距离”,或者“相对坐标系原点的某个方向和距离的点”。

这也就与我们上面所说的两个地点的相对位置描述一致了,只不过其中的一个点固定成了坐标系原点(0,0)

那么以下图为例,点(120,80)的向量含义就是“相对坐标系原点(0,0)的XX方向上移动YY距离的点”。而这就是屏幕坐标点与向量关系的由来。

平面坐标点的向量表示

那么这里的方向和距离到底是什么呢,如何计算?

从平面坐标点的向量表示到直角三角形

我们先说距离,平面坐标系上的任何一个点,它在X轴、Y轴投影与向量之间围成一个直角三角形(如上图右)。那么根据勾股定律,向量的长度 d = x 2 + y 2 d=\sqrt{x^2 + y^2} d=x2+y2

在GDScript中我们可以直接使用Vector2length()方法获取向量的长度,如果是屏幕点的位置的话,它所求的就是从屏幕坐标系原点到这个点的距离。

get_global_mouse_position().length()

除此之外,你可以用Vector2distance_to()求屏幕上任意两个点之间的距离

Vector2(100,100).distance_to(Vector2(200,200))

那么方向呢?方向我们单位向量来表示,单位向量是指长度为1的向量,计算的话就是**单位向量 = **向量/向量长度。在Godot中你同样可以省下这种计算,只用一个方法搞定,Vector2normalized()就是求一个向量的单位向量。

Vector2(100,100).normalized()

单位向量 = 长度为1的向量

单位向量的好处是,它的长度是1,1乘以任何数就是这个数本身,同时它又保存了向量的“方向”。那么我们就可以用单位向量乘以任何标量来“缩放向量”,同时也可以用**单位向量×长度(或叫距离)**来表示一个向量。也就回到了“在某个方向上偏移多少距离”的含义。

进一步的考虑在一个二维平面上,所有可能的方向都包含在一个中心点为坐标原点,半径为1的圆里。想想游戏手柄的摇杆和手机游戏的虚拟摇杆,你就应该明白,为什么它们可以控制你的游戏角色或视角移动了吧。

image.png
当然手柄的摇杆和手机游戏的虚拟摇杆还进一步检测了你拖动摇杆的力度,所以它不再是只包含圆周上的那些点,而是包含了圆周和圆内所有的点。

向量与移动

有了上面的基础,那么就应该研究物体在二维平面上的移动了。从A点到B点的移动可以理解为单纯的距离缩短。也可以描述为“A不停的向B移动,直到A和B重合(距离=0)”。
基于向量移动的基础示意图

那么体现在代码上就是需要知道A到B的方向和距离,然后定义单位时间内移动的速度,然后就可以移动了。

A到B的方向direction可以用A.direction_to(B)求得,A到B的距离distance可以用A.distance_to(B)
定义单位时间的移动距离,也就是速度speed,那么速度向量velocity = direction * speed,也就是方向*移动距离

不要懵了,directiondistancevelocity是初学者学习基础移动必须学会的三个单词:

  • direction:方向,申明变量时可以简写为dir
  • distance:距离,申明变量时可以简写为disd
  • velocity:(沿某一方向的)速度,申明变量时可以简写为vecv

无论是申明变量还是使用Vector2的方法你都会遇到这三个单词。

整个原理就是先判断起始点到目标点的距离是否大于0,然后将A的位置加上速度向量velocity,移动一段距离,然后循环,直到距离=0。

具体可以参阅相关的示例。

这是用纯向量方法移动物体的形式,Godot中移动物体和实现碰撞需要用物理体那套,但是基础的基于向量的移动是必学的基础,它在某些时候会有用处。

局部坐标系

就像人类曾经经历了地心说和日心说,再到现在的宏大宇宙观,中心与原点有相似性,它也可以因为不同的认识和参照定义而发生变化。

你可以将二维平面的任意一点作为原点构造一个平面坐标系。但是你或者别人完全可以选择除了这一个点之外的任何一个点的位置重新建立一个坐标系。同一个点会因为你设立的坐标系不同而有不同的坐标值表示。

同时在二维平面的某个局部,你又可以创建一个局部坐标系。局部坐标系的好处是,它可以只描述局部范围内的内容,而忽略其他的东西。

相对的你可以将它的上一级坐标系称为“全局坐标系”,当然这很违心,因为“全局坐标系”本质上也是一个“局部坐标系”,因为它的外面可以有更大的坐标系。

就像地球的经纬度系统就可以看做是地球的全局坐标系,但是在太阳系乃至更大的银河系和宇宙来说,坐标系还可以随着讨论范围逐渐扩大。

但是在Godot里,一般情况下你就可以认为屏幕坐标系就是“最大”的坐标系,是“全局坐标系”,一个场景的根节点其“局部坐标系”默认与“全局坐标系”重合,除非你不移动它。而每一层的子节点,都有一个自己的“局部坐标系”。

在Godot的API中也体现了全局位置、缩放、旋转和局部位置、缩放、旋转的概念。
全局坐标系与局部坐标系

极坐标系

大致说完平面坐标系和二维向量,再加入一点极坐标和三角函数的内容。在一个平面上表示一个点的位置,不止有平面坐标系法和向量法,还有极坐标系法。

极坐标系(polar coordinates)是指在平面内由极点、极轴和极径组成的坐标系。在平面上取定一点O,称为极点。从O出发引一条射线Ox,称为极轴。再取定一个单位长度,通常规定角度取逆时针方向为正。这样,平面上任一点P的位置就可以用线段OP的长度ρ以及从Ox到OP的角度θ来确定,有序数对(ρ,θ)就称为P点的极坐标,记为P(ρ,θ);ρ称为P点的极径,θ称为P点的极角。
点的极坐标表示
它的本质是规定一个正方向,表示0度,然后平面内的某个点就可以表示为方向为与这个正方向的夹角,距离为原点到这个点的距离,两个参数确定一个点。依然是方向和距离,但是用了角度和长度。
在Godot中可以理解为,2D节点的朝向,也就是rotation_degree属性,遵循了这种坐标系,但是依然是反的,它的角度顺时针方向取正,逆时针方向取负。并且rotation_degree属性的单位是度,而不是弧度。

向量的夹角

在GDScript中,你可以用Vector2angle()求某个向量与X轴正方向的夹角。

print(Vector2(100,100).angle()) # 0.785398,单位:弧度

因此你完全可以将一个二维坐标系点的位置,通过封装(到原点的距离,与X轴正方向夹角(弧度))来获得其极坐标表示。

你也可以用A.angle_to(B)求两个向量之间的夹角。
angle()和angle_to()
A.angle_to_point(B)由B到A的向量与X轴正方向的夹角。

A.angle_to_point(B)
B.angle_to_point(A)求由A到B的向量与X轴正方向的夹角。

B.angle_to_point(A)
在三者中,angle_to_point()应该是最让人费解的,但是平常也用不到,初学时不必深究。

向量的旋转

说完夹角再说旋转。向量除了加减乘除运算之外,还可以旋转。通过将一个向量旋转一定角度,可以获得一个新的向量。

向量的旋转有时候也很有用,比如在用Line2D节点绘制多边形或圆时,就会用到。

Line2D和PoolVector2Array

Line2D节点用于绘制路径,它的points属性存储构成路径的所有顶点,是PoolVector2Array类型。
PoolVector2Array你可以简单理解为只存储Vector2类型的特殊数组。
image.png
你可以直接在编辑器中手动绘制路径的顶点。注意它记录的是全局坐标,也就是基于屏幕左上角的位置,与自身的层级和局部坐标无关。
在编辑器中手动绘制Line2D的路径顶点
绘制后你可以在“检视器”面板展开points属性,看到其中记录的顶点坐标。

points中记录的顶点坐标
同样的你可以用代码生成这些顶点坐标,而通过旋转向量的方法,我们可以用Line2D绘制圆、圆弧等等。

绘制多边形和圆

策略很简单:

  • 指定细分数,或者多边形的边数,然后我们用360/边数获得单次要旋转的角度
  • 指定旋转半径,我们将X轴正方向的单位向量Vector2.RIGHT乘以旋转半径就获得了我们初始要旋转的向量
  • 然后我们将它旋转指定的角度,并将旋转得到的新的点的位置加入到一个PoolVector2Array中,通过多次旋转就得到了多个点
  • 最后我们赋值Line2Dpoints属性为这个PoolVector2Array,搞定!
extends Line2D

var subdivision = 6 # 细分数
var r = 100 # 旋转半径
var center = Vector2(400,200) # 旋转中心


func _ready():
	width = 2
	
	var pots:PoolVector2Array = []
	var uint = Vector2.RIGHT # 向右的单位向量
	var per_angle = (2 * PI) / subdivision # 单次旋转角度
	var basic_vec = uint * r  # 要旋转基础向量
	pots.append(basic_vec + center)
	for i in range(1,subdivision+1):
		pots.append(basic_vec.rotated(per_angle * i)  + center)
	pots.append(basic_vec + center) # 回到第一个点,闭合
	points = pots
	pass

上面的代码运行后绘制的就是一个中心点在(400,200),中心点到每个顶点的距离是100像素的正六边形。
在这里插入图片描述

圆和正多边形的唯一区别就是细分数,只要细分数达到一定的数量,就会“以直求曲”,从折线变成近似曲线的效果,这也是很多计算机软件里绘制圆形的奥秘。
在这里插入图片描述

绘制圆弧和扇形

学会了画圆,那么圆弧和扇形也就没有什么困难了。

extends Line2D

var subdivision = 5 # 细分数
var start_angle = deg2rad(45) # 起始角度
var end_angle = deg2rad(90) # 起始角度
var r = 100 # 旋转半径
var center = Vector2(400,200) # 选中中心


func _ready():
	width = 2
	
	var pots:PoolVector2Array = []
	var uint = Vector2.RIGHT # 向右的单位向量
	var d_angle = end_angle - start_angle if end_angle - start_angle >=0 else start_angle - end_angle
	var per_angle = d_angle / subdivision # 单次旋转角度
	var basic_vec = uint * r  # 要旋转基础向量
	
	print(basic_vec.rotated(start_angle))
	pots.append(center) # 添加中心点
	var start_point = basic_vec.rotated(start_angle) + center # 起始角度点
	pots.append(start_point) # 添加起始点
	
	for i in range(1,subdivision+1):
		pots.append(basic_vec.rotated(start_angle + per_angle * i)  + center)
	pots.append(center) # 回到中心点,闭合
	points = pots
	pass

在这里插入图片描述

上面的代码如果首尾都不加中心点,就变成了圆弧。

螺旋线

上面举例的都是只旋转,但是旋转半径不变的情况,但你完全可以尝试一下按参数规律变化半径的螺旋线之类的。

三角函数

涉及平面坐标系、位置和角度,那么三角函数也是躲不过去的一个话题,这里我只简单的说一下,知道半径和角度,如何计算坐标。其实一张图就够了,你可以自己思考一下为什么。
image.png
关于向量其实想说的还很多,但是限于篇幅和经历,这次只说这么多,希望对新手有所帮助。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1541413.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

WSL使用

WSL使用 WSL安装和使用 Termianl和Ubuntu的安装 打开Hype-V虚拟化配置Microsoft Store中搜索Window Terminal并安装Microsoft Store中搜索Ubuntu, 选择安装Ubuntu 22.04.3 LTS版本打开Window Terminal选择Ubuntu标签栏, 进入命令行 中文输入法安装 查看是否安装了fcitx框架…

2023第13届上海生物发酵展8月7-9日举办

2024第13届国际生物发酵产品与技术装备展(上海展) 2024年8月7-9日|上海新国际博览中心 主办单位: 中国生物发酵产业协会 承办单位: 上海信世展览服务有限公司 院校支持: 北京工商大学 大连工业大学 华东理工大…

FakeLocation报虚拟位置服务连接失败,请重启设备再试

虚拟位置服务连接失败,请重启设备再试 最近遇到一个手机软件报的bug“虚拟位置服务连接失败,请重启设备再试” 因为我的实体“虚拟机”已经root,按道理是不可能报这个错的 折腾了2天,终于解决了 原来是这样,安装最新…

React腳手架已經創建好了,想使用Vite作為開發依賴

使用Vite作為開發依賴 安裝VITE配置VITE配置文件簡單的VITE配置項更改package.json中的scripts在根目錄中添加index.html現在可以瀏覽你的頁面了 安裝VITE 首先,在現有的React項目中安裝VITE npm install vite --save-dev || yarn add vite --dev配置VITE配置文件 …

【MySQL】复合查询——基本单表查询、多表查询、自连接、子查询、使用from进行子查询、合并查询

文章目录 MySQL复合查询1. 基本单表查询2. 多表查询3. 自连接4. 子查询4.1 单行子查询4.2 多行子查询4.3 多列子查询4.4 使用from进行子查询 5. 合并查询5.1 union5.2 union all MySQL 复合查询 数据库的复合查询是指在一个查询中结合使用多个查询条件或查询子句,以…

常见技术难点及方案

1. 分布式锁 1.1 难点 1.1.1 锁延期 同一时间内不允许多个客户端同时获得锁; 1.1.2 防止死锁 需要确保在任何故障场景下,都不会出现死锁; 1.2.3 可重入 特殊的锁机制,它允许同一个线程多次获取同一个锁而不会被阻塞。 1.2…

五、分布式锁-redission

源码仓库地址:gitgitee.com:chuangchuang-liu/hm-dingping.git 1、redission介绍 目前基于redis的setnx特性实现的自定义分布式锁仍存在的问题: 问题描述重入问题同一个线程无法多次获取统一把锁。当方法A成功获取锁后,调用方法B&#xff0…

【C++】如何用一个哈希表同时封装出unordered_set与unordered_map

👀樊梓慕:个人主页 🎥个人专栏:《C语言》《数据结构》《蓝桥杯试题》《LeetCode刷题笔记》《实训项目》《C》《Linux》《算法》 🌝每一个不曾起舞的日子,都是对生命的辜负 目录 前言 1.哈希桶源码 2.哈希…

19.删除链表的倒数第N个结点 92.反转链表II

给你一个链表,删除链表的倒数第 n 个结点,并且返回链表的头结点。 示例 1: 输入:head [1,2,3,4,5], n 2 输出:[1,2,3,5]示例 2: 输入:head [1], n 1 输出:[]示例 3: …

模拟-算法

文章目录 替换所有的问号提莫攻击Z字形变换外观数列数青蛙 替换所有的问号 算法思路: 从前往后遍历整个字符串,找到问号之后,就遍历 a ~ z 去尝试替换即可。 class Solution {public String modifyString(String s) {char[] ss s.toCharA…

删除字符串--给你一个字符串S,要求你将字符串中出现的所有“gzu“子串删除,输出删除之后的S。

输入描述: 输入一行字符串S&#xff0c;长度不超过100。 输出描述: 输出进行删除操作之后的S。 #include <stdio.h> #include <stdlib.h> #include <string.h>//结合了串的模式匹配算法思路int main(){char s[100];char a[3]{g,z,u};gets(s);int nstrlen…

数据库语言一些基本操作

1&#xff0c;消除取值重复的行。 例如&#xff1a;查成绩不及格的学号&#xff1a;SELECT DISTINCT sno FROM SC WHERE grade<60. 这里使用DISTINCT表示取消取值重复的行。 2&#xff0c;比较。 例如&#xff1a;查计算机系全体学生的姓名&#xff1a;SELECT Sname FROM…

C++一维数组练习oj(3)

为什么C的一维数组练习要出要做那么多的题目&#xff1f;因为我们是竞赛学生&#xff01;想要将每个知识点灵活运用的话就必须刷大量的题目来锻炼思维。 我使用的是jsswoj.com这个刷题网站&#xff0c;当然要钱... C一维数组练习oj(2)-CSDN博客这是上一次的题目讲解 这道题有…

java每日一题——买啤酒(递归经典问题)

前言&#xff1a; 非常喜欢的一道题&#xff0c;经典中的经典。打好基础&#xff0c;daydayup!!!啤酒问题&#xff1a;一瓶啤酒2元&#xff0c;4个盖子可以换一瓶&#xff0c;2个空瓶可以换一瓶&#xff0c;请问10元可以喝几瓶 题目如下&#xff1a; 啤酒问题&#xff1a;一瓶…

[Halcon学习笔记]在Qt上实现Halcon窗口的字体设置颜色设置等功能

1、 Halcon字体大小设置在Qt上的实现 在之前介绍过Halcon窗口显示文字字体的尺寸和样式&#xff0c;具体详细介绍可回看 &#xff08;一&#xff09;Halcon窗口界面上显示文字的字体尺寸、样式修改 当时介绍的设定方法 //Win下QString Font_win "-Arial-10-*-1-*-*-1-&q…

传输层——UDP协议

端口号(Port) 端口号标识了一个主机上进行通信的不同的应用程序&#xff0c;准确来说&#xff0c;端口号标识了主机上唯一的一个进程。 在TCP/IP协议中, 用 "源IP", "源端口号", "目的IP", "目的端口号", "协议号" 这样一个…

罗德与施瓦茨联合广和通全面验证RedCap模组FG132系列先进性能

近日&#xff0c;罗德与施瓦茨联合广和通完成Redcap(Reduce Capability)功能和性能验证。本次测试使用R&SCMX500 OBT(One Box Tester)无线通信测试仪&#xff0c;主要验证广和通RedCap模组FG132系列射频性能以及IP层吞吐量&#xff0c;包括RedCap上下行吞吐量和射频指标如矢…

Java 自定义线程池实现

自定义线程池 简介任务图示阻塞队列 BlockingQueue<T>ReentrantLock代码 线程池 ThreadPool工作线程类 Worker 拒绝策略接口代码测试类 TestThreadPool为什么需要j i&#xff1f;&#xff08;lambad表达式相关&#xff09; 测试结果拒绝策略&#xff1a;让调用者自己执行…

c++常考基础知识(2)

二.c关键字 关键字汇总 c中共有63个关键字&#xff0c;其中包括int&#xff0c;char&#xff0c;double等类型关键字&#xff0c;if&#xff0c;else&#xff0c;while&#xff0c;do&#xff0c;等语法关键字&#xff0c;还有sizeof等函数关键字。 三.数据结构 1.数组&#x…

Navicat 干货 | 探索 PostgreSQL 的外部数据包装器和统计函数

PostgreSQL 因其稳定性和可扩展性而广受青睐&#xff0c;为开发人员和数据管理员提供了许多有用的函数。在这些函数中&#xff0c;file_fdw_handler、file_fdw_validator、pg_stat_statements、pg_stat_statements_info 以及 pg_stat_statements_reset 是其中的重要函数&#x…