6边形网格地图,格子间的距离计算
这样一个6边形网格地图,有没有公式计算两个格子间的距离呢?
比如(0,0)走到(1,1)需要走几步,我们看图可以发现走两步就行了,但是有公式可以计算就更好了。
下面跟群里的小伙伴讨论之后的出的结果。
我们需要借助三维坐标
像这样的坐标体系,有x,y,z三个维度,x,y,z的和始终是0,我们称之为cube坐标,距离计算公式是
distance=(abs(cubePos1.x-cubePos2.x)+abs(cubePos1.y-cubePos2.y)+abs(cubePos1.z-cubePos2.z))/2
或者
diatance=max(cubePos1.x-cubePos2.x)+abs(cubePos1.y-cubePos2.y)+abs(cubePos1.z-cubePos2.z))
上面两个公式都能算出距离,至于具体原理我就不懂了,反正可以这么用
然后要做的就是把二维坐标转成三维坐标
其实很简单
我们原来的坐标是这样的
0,0 --- 1,0 --- 2,0 --- 3,0
--- 0,1 --- 1,1 --- 2,1 --- 2,2
0,2 --- 1,2 --- 2,2 --- 3,2
--- 0,3 --- 1,3 --- 2,3 --- 3,3
如果调整成这样
0,0 --- 1,0 --- 2,0 --- 3,0
--- 0,1 --- 1,1 --- 2,1 --- 3,1
--- --- 0,2 --- 1,2 --- 2,2 --- 3,2
--- --- --- 0,3 --- 1,3 --- 2,3 --- 3,3
这样二维中的x,y就对应了三维中的x,y。至于三维的z,因为x,y,z的和始终是0,只要z=-x-y就行了
调整过程其实就是x=x-int(y/2)
最终我们得到二维坐标转三维坐标的函数
func getCubePosFromPos(pos): var x=pos.x-int(pos.y)/2 var y=pos.y var z=-x-y return Vector3(x,y,z)
计算距离的函数
func posDistance(pos1,pos2): var cubePos1=getCubePosFromPos(pos1) var cubePos2=getCubePosFromPos(pos2) var distance=(abs(cubePos1.x-cubePos2.x)+abs(cubePos1.y-cubePos2.y)+abs(cubePos1.z-cubePos2.z))/ 2 return distance
把这个公式放到游戏中试验下
黑色小圆圈里的数字表示这个格子到主角的距离,计算很准确
顺便说下从三维坐标转回到二维坐标的算法
func getPosFromCubePos(cubePos): var y=cubePos.y var x=cubePos.x+int(y)/2 return Vector2(x,y)
参考资料
https://www.redblobgames.com/grids/hexagons/
笔记:个人对于godot中yield的一些理解
研究了一下yield,做个笔记
- yield是关键字。
- yield会返回一个对象,类型是GDScriptFunctionStat
这个对象保存了中断时函数的状态,这个对象有个函数resume(),可以继续执行中断的函数。
这个对象运行完成会发出completed信号,有了这个信号就可以实现yield的嵌套。
extends Node #嵌套演示 func _ready(): funA() func funA(): print("a_start") yield(funB(),"completed") print("a_end") func funB(): print("b_start") yield(get_tree().create_timer(2),"timeout") print("b_end")
结果是先输出a_start,b_start,等待2秒后输出b_end,a_end
- yield(target,signal)
目前猜测,由于yield返回的是一个GDScriptFunctionStat实例,该类继承reference,没有引用会被回收。
所以必须有个对象保持它的引用,由于信号没有保持它的引用,所以是target保持了它的引用。
target在emit_signal时会顺便检测有没有GDScriptFunctionStat的实例需要resume,如果有就resume它。
这就实现了yield等待信号的效果。
yield可以传递参数
extends Node signal sig func _ready(): var a=yield(self,"sig") print(a) func _process(delta): if Input.is_action_just_pressed("ui_accept"): emit_signal("sig",10)
输出结果是10.
yield也可以用来传递参数,可以实现选择的效果。
更加灵活的yield方式,可以被打断或者做其他操作
extends Node var funcState var die=false func _ready(): funcState=testFunc() func testFunc(): print(1) wait(2) yield() print(2) wait(2) yield() print(3) func wait(time): get_tree().create_timer(time).connect("timeout",self,"onTimeOut") func onTimeOut(): if !die: funcState=funcState.resume() //死亡就不会继续运行,被打断了
目前想到这些,yield用来做剧情应该是没问题的
做技能系统还有待研究
欢迎讨论
Godot的Shader入门+制作像素文字Shader
Shader在哪写?
如图,新建一个sprite,在它的Meterial属性里点击新建shaderMaerial,在shaderMateril里点击新建shader,就会弹出着色器面板,可以在里面写shader了。
Shader格式
第一行我们先写上:
shader_type canvas_item; void fragment(){ }
shader_type是shader的类型,有spatial,canvas_item和particles 三种。必须在第一行指定shader_type。
spatial: 用来渲染3D
canvas_item: 用来渲染2D
particles:用来渲染粒子
fragment函数里面就是我们要写处理逻辑的地方。
fragment()是shader的回调函数,
每一个像素点都会运行一次fragment函数。
每一个像素点都会运行一次fragment函数。
每一个像素点都会运行一次fragment函数。
重要的事说三遍。
当然你会发现语法和gdscript有些不同,语句结尾要加分号,函数要用大括号括起来。
详细的请参考官方文档。
Shader基本逻辑
全白效果
先从简单的开始
shader_type canvas_item; void fragment(){ COLOR=vec4(1.0,1.0,1.0,1.0); }
效果如图,整个sprite都变白了。
COLOR是shader内置属性,它表示像素点的颜色。
vec4 是shader的数据类型,4维向量,可以用来表示颜色。
vec4有四个属性,x,y,z,w。每个属性都是float类型(所以用1.0而不是1)。
vec4的四个属性也可以用r,g,b,a来表示,就是说vec4.x和vec4.r是完全一样的。
shader常用类型有float,vec2,vec3,vec4等。
现在我们来看COLOR=vec4(1.0,1.0,1.0,1.0);这句话,
vec4(1.0,1.0,1.0,1.0)表示rgba都是1.0的颜色,就是白色。
每个像素点都会运行一次这个语句,所以每个像素点的颜色都变成了白色。
剪影效果
我们先写上
shader_type canvas_item; void fragment(){ COLOR=texture(TEXTURE,UV); }
效果如图
你可能会想:咦?这不是和原来一样吗。
是的,确实和原来一样。这句话是什么意思呢?
UV 是shader内置属性,用来表示当前像素点的坐标。它是一个vec2。
每个像素点都有一个UV值,每个像素点的UV值不一样。
左上角的像素点UV值是(0.0,0.0),
右下角的像素点UV值是(1.0,1.0),
正中心的像素点UV值是(0.5,0.5)。
TEXTURE是shader的内置属性(反正这些全大写的都是内置属性)。它表示原始的纹理。
texture函数是shader的内置函数,传入纹理和uv值,可以返回纹理上uv坐标的颜色(vec4)。
我们来看:COLOR=texture(TEXTURE,UV);
就是每个像素获取自己原来的颜色,赋值给COLOR。当然不会有任何变化。
接下来我们写
shader_type canvas_item; void fragment(){ COLOR=texture(TEXTURE,UV); COLOR.rgb=vec3(1.0,1.0,1.0); }
效果如图,成功实现了剪影效果。
COLOR.rgb=xxx 这是shader中的语法,可以同时给r,g,b三个值赋值,非常方便。
你可能会说,为什么要写COLOR=texture(TEXTURE,UV);?
我不能直接写 COLOR.rgb=vec3(1.0,1.0,1.0);吗?
这是不行的,COLOR默认值其实是(1.0,1.0,1.0,1.0)。
只要调用了COLOR,COLOR就会变成(1.0,1.0,1.0,1.0)。
你可以试试这样
shader_type canvas_item; void fragment(){ COLOR; }
如图,也能让图片变全白。
制作像素化文字shader
我这里用的字体是思源黑体Medium。新建一个label,字体大小设成12,我们看看默认的效果。
放大后:
为什么看上去这么不像素,主要是字体的抗锯齿,使它有半透明的部分。
所以要做的就是去掉半透明的部分。
学习了上面的知识,你应该知道怎么做了。
在label的Meterial属性里点击,新建ShaderMeterial,然后新建Shader
Shader代码如下
shader_type canvas_item; void fragment(){ COLOR=texture(TEXTURE,UV); if(COLOR.a<0.5){ COLOR.a=0.0; }else{ COLOR.a=1.0; } }
如果像素的alpha<0.5,就把alpha变成0。如果像素的alpha>=0.5,就把alpha变成1。
看看效果
放大后:
很好很像素。
当然临界值可以不是0.5,如果我们希望可以微调临界值该怎么办?
这时要声明变量,代码如下
shader_type canvas_item; uniform float limit=0.5;//声明变量 limit,默认值是0.5 void fragment(){ COLOR=texture(TEXTURE,UV); if(COLOR.a<limit){ //把0.5替换成limit COLOR.a=0.0; }else{ COLOR.a=1.0; } }
用uniform指定的变量可以在外部修改,可以微调像素化的效果。
最后点击保存按钮把shader保存成文件,就可以重复使用了
更多关于shader的信息请参考官方文档:
http://docs.godotengine.org/zh_CN/latest/tutorials/shading/shading_language.html
Godot中锚点(anchor)和边界(margin)的用法
在godot中,Control类都有anchor和margin属性。
这两个属性是给ui自适配用的。
每个control都有四个边,上(top),下(bottom),左(left),右(right)
anchor(锚点):表示每条边的参照值。这个参照值是相对于父节点的百分比取值,一般会取0,0.5,1这三个值。
以anchor_left属性为例,
anchor_left=0表示当前节点的左边界以父节点的左边界为参照值,
archor_left=0.5表示当前节点的左边界以父节点的中心为参照值,
archor_left=1表示当前节点的左边界以父节点的右边界为参照值。
margin(边界):表示每条边和参照值得相对位置,这个值的就是像素距离。
举例来说明比较好理解,我新建了一个Panel,一个TextureRect
1.我希望TextureRect和Panel的边界始终保持50个像素
TextureRect的Anchor和Margin要这么设置
2.接下来新建个按钮,是它始终位于右上方,长度保持100,高度保持50
按钮的anchor和margin属性
3.接下来新建个开始游戏按钮,长100,宽50,x方向居中,y方向距离底面50的距离
开始游戏按钮的anchor和margin
顺便说一下布局其实就是一些常用的anchor和margin的组合
在Godot中制作杀戮尖塔的箭头
杀戮尖塔里面使用卡牌时的箭头是这样的:
箭头的形态非常符合贝塞尔曲线
PS中的钢笔工具就是用的贝塞尔曲线
如图,一条贝塞尔曲线需要用四个点来确定,一个起点,一个终点,加上两个控制点。
我们把四个点分别命名:起点(startPos),终点(endPos),控制点A(ctrlAPos),控制点B(ctrlBPos)
贝塞尔曲线的公式是:
position=startPos*(1-t)*(1-t)*(1-t)+3*ctrlAPos*t*(1-t)*(1-t)+3*ctrlBPos*t*t*(1-t)+endPos*t*t*t
四个点都确定后,公式里的t就是唯一的变量,t是指从起点到终点的百分比,取值是0-1。
比如0代表曲线起点,0.2代表曲线从起点开始20%的位置,0.5代表曲线中间位置,1代表曲线终点。
公式的计算结果position就是当前t值所对应的曲线上的点。
不过在游戏中我们是使用两个点来确定曲线的,卡牌所在位置是曲线的起点,鼠标所在位置是曲线的终点。
那么两个控制点就要根据起点和终点来进行计算。
杀戮尖塔里的曲线大致是这样:
我们可以大致的写出控制点的计算公式:
ctrlAPos.x=startPos.x+(startPos.x-endPos.x)*0.2
ctrlAPos.y=endPos.y-(endPos.y-startPos.y)*0.2
ctrlBPos.x=startPos.x-(startPos.x-endPos.x)*0.2
ctrlBPos.y=endPos.y+(endPos.y-startPos.y)*0.2
当然这个计算公式可以自己微调,使曲线更符合自己想要的形态
理解了曲线的原理,现在开始在godot中实现。
我画了两个箭头,箭头1和箭头2,如图。
在godot中新建一个场景,新建Node2D,命名为贝塞尔箭头。添加脚本。
开始写脚本。首先我们的箭头有20节,我们需要在初始化的时候准备好。
之后更新箭头时重新排好每一节就能形成一条曲线。
extends Node2D var list=[] #数组,用来保存20节小箭头 func _ready(): #生成19节尾巴小箭头,用箭头1的图片 for i in range(19): var sprite=Sprite.new() #新建Sprite节点 add_child(sprite) #添加到场景里 list.append(sprite) #添加到数组里 sprite.texture=load("res://Sprites/箭头1.png") #把图片换成箭头1 sprite.scale=Vector2(1,1)*(0.2+float(i)/18*0.8) #改变缩放,根据杀戮尖塔,箭头是一节节越来越大的 sprite.offset=Vector2(-25,0) #由于我画的图片中心点在箭头中间, #这里改变一下图片偏移,把图片中心点移动到箭头头部 #最后生成终点的箭头,用箭头2的图片 var sprite=Sprite.new() add_child(sprite) list.append(sprite) sprite.texture=load("res://Sprites/箭头2.png") sprite.offset=Vector2(-25,0)
然后我们需要一个函数来设置箭头的起点和终点
func reset(startPos,endPos): #根据传入的起点和终点来计算两个控制点 var ctrlAPos=Vector2() var ctrlBPos=Vector2() ctrlAPos.x=startPos.x+(startPos.x-endPos.x)*0.1 #这里我把参数做了微调,感觉这样更加符合杀戮尖塔的效果 ctrlAPos.y=endPos.y-(endPos.y-startPos.y)*0.2 ctrlBPos.y=endPos.y+(endPos.y-startPos.y)*0.3 ctrlBPos.x=startPos.x-(startPos.x-endPos.x)*0.3 #根据贝塞尔曲线重新设置所有小箭头的位置 for i in range(20): var t=float(i)/19 var pos=startPos*(1-t)*(1-t)*(1-t)+3*ctrlAPos*t*(1-t)*(1-t)+3*ctrlBPos*t*t*(1-t)+endPos*t*t*t list[i].position=pos #虽然更改了箭头的位置,不过还需要重新计算箭头的方向 updateAngle() #重新计算所有箭头的方向
接下来我们需要完成updateAngle()这个函数
思路是每个小箭头根据前一个箭头和自己的位置来计算角度
func updateAngle(): for i in range(20): if i==0: list[0].rotation_degrees=270 #第一个小箭头就让他固定朝上好了 else: var current=list[i] #当前的小箭头 var last=list[i-1] #前一个小箭头 var lenVec=current.position-last.position #两个箭头连线的向量 var a=lenVec.angle() #计算这个向量的角度,这个angle()返回值是弧度 a=rad2deg(a) #弧度转成角度 current.rotation_degrees=a #更新小箭头的方向
我们的贝塞尔箭头就做好了,外界只要调用reset方法就能更新箭头,
不需要用的时候可以用visible=false把它隐藏掉
我把它放到游戏中看看效果
非常完美!
Godot中如何用数字表示collision_mask
先用二进制写出来,1代表激活,0代表没激活,最右边代表第一层。然后转换成十进制。
举例说明:
二进制是1,转成十进制是1,collision_mask=1。
二进制是1101,转成十进制是13,collision_mask=13。
二进制1000001100001转成十进制是4193,collision_mask=4193。
其他类似的比如light_mask,collision_layer都是这种写法。
Godot中的分贝和音量换算
Godot中不管是音频播放器还是音频总线都是用分贝来表示音量大小的,如图
我们希望能用0-1来表示音量大小,如何转换呢?
我们用v表示音量大小(0-1),以下是转换公式
V转换成volume_db的公式:
volume_db=10*log(v)/log(10)
但是log的参数必须是大于0的,我们要做一下特殊判断,最终公式为:
If v<=0:
volume_db=-60
else:
volume_db=10*log(v)/log(10)
Volume_db只要不大于-60,系统就不会发出声音了。这个在设置里可以设定不发出声音的临界值,默认是-60,所以这里就让volume_db=-60。
一般我们会用全局变量来保存v的值,不需要用volume_db转v的公式,不过还是说一下
volume_db转换成v,公式是:
v=pow(10,(float(volume_db)/10))
注意里面的float(),这里我额外说一下Godot里计算的坑。
举例:
var a=1
print(a/10)
输出结果为0,因为a是整数,计算结果默认取整了。
如果加上float先把a变成浮点数:
Var a=1
print(float(a)/10)
输出结果就是0.1。
Godot使用Light2D实现遮罩效果
先来看看效果:
用鼠标拖动圆,数字和血手印只会显示在圆的范围内。
用light2D很简单能做出这个效果。
这是demo中用到的素材,如图
首先我们新建一个项目,导入素材,新建场景,新建sprite,把背景放进去
然后放入图片2333,这时候2333是正常显示的
在2333的属性面版Material这里选择新建CanvasItemMaterial
新建后点进去,设置CanvasItemMaterial,把LightMode改成Light Only
然后我们会发现2333已经看不到了,应为设置了LightOnly,2333只会在光照时显示,如图
接下来我们新建一个light2D,Texture设置为白色的圆
效果如图:
这时候还是显示白色的圆。我们更改一下光照的Mode为Mix,效果就出来了
不过仔细观察会发现,光照不只是照到了2333,也照到了背景的墙壁,和背景的墙壁混合产生了一个圆形的边缘。
如何才能让light2d只照到2333,而不照到背景呢?
答案是LightMask
选中2333,把LightMask设置成第二层,如图。
选中Light2D,把ItemCullMask设置成第二层
由于背景墙壁LightMask是默认的第一层,所以light2d只会照到2333.
如图,效果很不错
这时候我们就可以拖动light2d来观察了
现在我们来给他加上黑色边框
新建sprite,把素材黑圆边框放进去,把light2d拉到黑圆边框下面
把light2d的位置设为0,0
这样就完成了
源文件和素材我放到百度网盘了,有兴趣的可以自己做一下
https://pan.baidu.com/s/1s6AKTfCpgxiqrp4QP-oe0Q