GDScript 开发示例

[!tip] Godot 版本

包含部分3.x示例,后续都会以4.x为准。

Godot 官网:https://godotengine.org/

Godot 最新正式版下载:https://godotengine.org/download/windows/

所有版本下载:https://downloads.tuxfamily.org/godotengine/

将文件拖入窗口并加载文件内容

将文件拖入窗口并加载文件内容效果

创建场景

创建一个简单场景如下

将文件拖入窗口并加载文件内容场景

需要监听到文件的拖入事件,并且将文件内容读取写入到RichTextLabel中

脚本编写

extends Control

func _ready():
    get_tree().connect("files_dropped", self, "_on_file_drag")

func _on_file_drap(files, screen):
    for f in files:
        var file = File.new()
        file.open(file, File.READ)
        var content = file.get_as_text()
        $RichTextLabel.text += content

教你实现2D游戏中的小地图

效果演示

场景的移动,旋转,缩放都能够实时显示到小地图上

场景搭建

场景搭建

效果如下图

场景界面

并且我们将Camera2D的zoom属性设置为0.3,避免将背景地图全部照上。

勾选current属性,表示游戏窗口为当前的camera

勾选rotate属性,使得相机能够跟随player进行旋转

创建小地图

小地图节点

viewport: 能够单独映射出一个场景进行渲染,使用它来作为小地图的输出,右键点击viewport,点击Instance Child Scene连接之前创建的game场景

viewportContainer: 设置size大小属性,以及勾选Stretch选项,即可在窗口看到效果

viewportcontainer效果

人物移动

为player添加移动的时候就会发现场景在小地图中也同步更新了

extends Sprite

var speed = 300
var rotate_speed = 2

func _process(delta):
    var rotate_dir = 0
    if Input.is_action_pressed("rotate_left"):
        rotate_dir -= 1
    elif Input.is_action_pressed("rotate_right"):
        rotate_dir += 1
    rotation += rotate_speed * rotate_dir * delta

    var velocity = Vector2.ZERO
    if Input.is_action_pressed("ui_right"):
        velocity.x += 1
    if Input.is_action_pressed("ui_left"):
        velocity.x -= 1
    if Input.is_action_pressed("ui_up"):
        velocity.y -= 1
    if Input.is_action_pressed("ui_down"):
        velocity.y += 1
    velocity = velocity.normalized()
    position += velocity * speed * delta

小地图缩放

在场景根节点中创建脚本文件Main.gd, 添加小地图缩放功能,以及替换人物图标

extends Node2D

var defaultMinimapScaleNum = 0.9

onready var minimap_player = $CanvasLayer/Control/ViewportContainer/Viewport/minimap/player
onready var minimap_enemys = $CanvasLayer/Control/ViewportContainer/Viewport/minimap/enemys
onready var minimap_camera = $CanvasLayer/Control/ViewportContainer/Viewport/minimap/player/Camera2D

func _ready():
    setMinimapScale()

    minimap_player.set_texture(load("res://assets/player.png"))
    minimap_player.scale = Vector2(0.3, 0.3)

    for ene in miimap_enemys.get_children():
        ene.set_texture(load("res://assets/enemy.png"))
        ene.scale = Vector2(0.3, 0.3)

func _input(event):
    if event.is_action_pressed("scale_up"):
        if defaultMinimapScaleNum <= 1.8:
            defaultMinimapScaleNum += 0.1
    elif event.is_action_pressed("scale_down"):
        if defaultMinimapScaleNum >= 0.2:
            defaultMinimapScaleNum -= 0.1
    setMinimapScale()

func setMinimapScale():
    minimap_camera.zoom = Vector2(defaultMinimapScaleNum, defaultMinimapScaleNum)
    # 避免小地图的缩放影响到其他元素的大小
    minimap_player.scale = Vector2(defaultMinimapScaleNum * 0.3, defaultMinimapScaleNum * 0.3)
    for ene in miimap_enemys.get_children():
        ene.scale = Vector2(defaultMinimapScaleNum * 0.3, defaultMinimapScaleNum * 0.3)

教你实现简单的传送带效果

效果展示

场景搭建

效果展示

RigidBody2D是一个自带重力的节点

传送带样式

根据一张基础图片,进行repeat,生成传送带样式

创建一个Sprite节点,设置texture为AltasTexture类型,并将图片素材拖拽进texture中,选择吸附为Grid snap方便框选

在altasTexture中设置其宽度为图片宽度*10,使图片进行重复

效果展示

这个时候图片只是进行拉伸而不是复制,需要在图片的import面板,设置Flags -> Repeat为Enabled,然后选择Reimport重新导入即可

可以讲该AltasTexture进行保存为tres文件方便复用

传送带移动

通过设置texture的region -> position属性来完成传送带移动效果

extends StaticBody2D

var speed = 100

func _ready():
    # 设置该线速度,可以使得靠近该传送带的物体也跟着移动
    const_linear_velocity.x = speed

func _process(delta):
    $Sprite.texture.region.position.x -= speed * delta

教你实现漂浮文本效果

效果展示

漂浮文本节点

效果展示

创建控制脚本,实现漂浮效果,在初始创建给一个向上运动然后往下落的效果

extends Position2D

var text setget set_text,get_text

var velocity = Vector2.ZERO
var gravity = Vector2.ZERO
var mass = 100

func _process(delta):
    velocity += gravity * mass * delta
    position += velocity * delta

func set_text(txt):
    $Label.text = str(txt)

func get_text():
    return $Label.text

接下来我们需要为该节点实现缓慢消失的效果,使用Tween补间动画,控制Position2D节点的Visibility -> Modulate进行控制,并且在补间动画完成后将节点清除

extends Position2D

---
date: '2022-07-31'
tags: ['godot']
draft: false
---

# ....

func _ready():
    # 消失
    $Tween.interpolate_proerty(self, "modulate",
    Color(modulate.r, modulate.g, modulate,b, modulate.a),
    Color(modulate.r, modulate.g, modulate,b, 0),
    0.3, Tween.TRANS_LINEAR, Tween.EASE_OUT, 0.7
    )

    # 放大
    $Tween.interpolate_proerty(self, "scale",
    Vector2(0, 0),
    Vector2(1, 1),
    0.3, Tween.TRANS_LINEAR, Tween.EASE_OUT
    )

    # 缩小
    $Tween.interpolate_proerty(self, "scale",
    Vector2(1, 1),
    Vector2(0.4, 0.4),
    0.3, Tween.TRANS_LINEAR, Tween.EASE_OUT, 0.7
    )

    $Tween.start()

---
date: '2022-07-31'
tags: ['godot']
draft: false
---

# Tween所有动画完成信号
func _on_Tween_tween_all_completed():
    get_tree().queue_delete(self)

主场景

创建主场景,并且创建一个按钮,当点击后新建漂浮文本

extends Node2D

var float_text = preload("res://float.tscn")

func _on_Button_pressed():
    var ft = float_text.instance()
    ft.position = Vector2(200, 300)
    # 控制漂浮文本的数值
    ft.velocity = Vector2(rand_range(-50, 50), -130)
    ft.gravity = Vector2(0, 1.5)
    ft.mass(200)
    # 设置随机颜色
    ft.modulate = Color(rand_range(0.7, 1), rand_range(0.7, 1), rand_range(0.7, 1), 1)

    var num = randi()%10 - 5
    ft.text = num
    if num > 0:
        ft.text = "+" + ft.text

    add_child(ft)

教你实现PC和手机端的虚拟摇杆

效果展示

场景搭建

素材本身就是两个大小圆,主要是通过坐标检测是否在摇杆作用范围内,然后根据坐标计算更新中心小圆的位置,并且在松开手后将其归位

效果展示
extends Sprite

var maxLen = 70
var ondraging = -1 # 用于解决范围内点击然后移动到范围外能够继续滚动

func _input(event):
    if event is InputEventScreenDrag or (event is InputEventScreenTounch and event.is_pressed()):
        var mouse_pos = (event.position - self.global_position).length()

        if mouse_pos <= maxLen or event.get_index() == ondraging:
            ondraging = event.get_index() # 手指点击的索引
            
            $point.set_global_position(event.position)

            if get_point_pos().length() > maxLen:
                $point.set_position(get_point_pos().normalized() * maxLen)
    
    if event is InputEventScreenTounch and !event.is_pressed():
        # 松手
        set_center()
        if event.get_index() == ondraging:
            ondraing = -1

func get_point_pos():
    return $point.position

func set_center():
    # 添加缓动效果
    $Tween.interpolate_property($point, "position", get_point_pos(), Vector2(0, 0), 0.1, Tween.TRANS_LINEAR, Tween.EASE_IN_OUT)
    $Tween.start()

func get_now_pos():
    return get_point_pos().normalized()

移动映射

获取鼠标在摇杆上的位置,可以将这个位置映射到物体的移动上

extends Node2D

onready var joystick = $joystick

func _process(delta):
    $KinematicBody2D.move_and_slide(joystick.get_now_pos() * 230)

---
date: '2022-07-31'
tags: ['godot']
draft: false
---

# 将摇杆放置在按下的位置,而不是固定位置
func _input(event):
    if event is InputEventScreenTouch and event.is_pressed():
        joystick.visible = true
        joystick.position = event.position
    if event is InputEventScreenTouch and !event.is_pressed():
        joystick.visible = false

设置

需要注意在桌面环境中需要开启Emulate Touch From Mouse才能够响应InputEventScreenDrag, InputEventScreenTounch触摸事件

效果展示

如何将3D相机显示的画面展示到一个平面

显示相机内容示例

场景搭建

创建地板

3D场景创建地板

创建MeshInstance节点,并且将Mesh属性中选择PlaneMesh,然后在工具栏中使用缩放工具将其进行放大

修改地板颜色,可以在节点中选择Material,创建一个材质,在材质的Albedo属性中修改颜色属性即可

创建其他3D图形

创建MeshInstance节点,并且将Mesh属性中选择CubeMesh,创建一个立方体,然后可以根据喜好自行修改大小

3D场景示例

创建主相机

新建Camera3D节点,调整窗口显示位置后,选中Camera,然后点击窗口左上角的Perspective下拉菜单,选中Align Transform with View以及Align Rotation with View即可将相机固定到当前视窗的视角

快速设置相机角度

创建辅助相机

将辅助相机的视图作为渲染输出到实时屏幕中,因此我们首先需要先新建一个viewport节点,viewport可以将子节点的内容输出到屏幕中渲染,然后在viewport节点下新建Camera3D节点。viewport的size属性设置为所需的大小,在这个示例中设置的是500

然后再新建一个Sprite3D,作为具体的输出屏幕,在该节点的Texture属性中选择ViewportTexture,将其连接到之前创建的ViewPort中

相机视图输出 相机视图输出运行效果

这里可以看到进行了翻转,所以需要在Sprite3D节点中勾选Flip V即可正常显示

如何实现2D场景的AI寻路

实现效果

路径绘制

路径绘制

点击Polygon2D节点,然后在窗口上方点击添加路径按钮,开始绘制可以活动的范围,效果图如下

路径绘制效果

Polygon2D创建的路径赋值给NavigationPolygon寻路节点, 需要在根节点Navigation2D节点中创建一个脚本

extends Navigation2D

func _ready():
    var polygon = NavigationPolygon.new()
    var outline = $Polygon2D.polygon
    polygon.add_outline(outline)
    polygon.make_polygons_from_outlines()
    $NavigationPolygonInstance.navpoly = polygon
    set_process(false) # 暂停行为逻辑

寻路逻辑

场景中的Icon节点就是需要响应鼠标事件,并且使其自动移动到目标地址

extends Navigation2D

var path = []
var speed = 300

func _input(event):
    if event is InputEventMouseButton and event.is_pressed() and event.button_index == BUTTON_LEFT:
        path = get_simple_path($icon.position, get_local_mouse_position(), true) # 计算出来运动路径
        # $Polygon2D.draw_path_line(path) # 绘制出运动轨迹,具体实现看下文
        set_process(true) # 只有在有左键点击事件才恢复渲染

func _process(delta):
    var walk_speed = speed * delta
    move_to_path(walk_speed)

func move_to_path(walk_speed):
    var last_point = $icon.position
    while path.size():
        var distance_between_points = last_point.distance_to(path[0])
        if distance_between_points >= 2:
            $icon.position = last_point.linear_interpolate(path[0], walk_speed/distance_between_points)
            return # 返回,等待_process执行下一次移动
        last_point = path[0]
        path.remove(0)
    # 移动到了目标路径,将最后一点的偏差直接赋值即可
    $icon.position = last_point
    set_process(false)

绘制运动路径

通过脚本绘制出物体的运动轨迹

路径效果

在Polygon2D上创建脚本如下

extends Polygon2D

var path = []

func draw_path_line(path):
    self.path = path
    update()

func _draw():
    if path.size() > 1:
        for i in range(0, path.size() - 1, 1):
            draw_line(path[i], path[i+1])
            update()

实现2D物品的拖拽移动

拖拽物体实现效果

创建场景

创建Area2D节点,并在该节点下新建Sprite2D作为需要拖动的物品,将图片拖拽至sprite2D的texture中

为该sprite2D创建collisionShape2D节点,在collisionShape2D节点的shape属性中选择形状为方形RectangleShape2D,并调整大小与sprite一致

拖拽物体节点结构

脚本编写

为Area2D创建脚本,脚本功能包括

  • 检测鼠标是否点击到了物体
  • 将物品的位置移动到鼠标所在位置
  • 为物品的点击添加偏移,点击哪个地方就以哪个地方作为移动原点
extends Area2D

var isDrag = false
var offset = Vector2.ZERO

func _process(delta):
    if isDrag:
        self.position = get_global_mouse_postion() + offset

func _input_event(viewport, event, shape_idx):
    if event is InputEventMouseButton and event.button_index == BUTTON_LEFT:
        if event.is_pressed():
            offset = self.position - get_global_mouse_position()
            isDrag = true
        else:
            isDrag = false

实现2D相机的视角控制

脚本编写

该示例场景比较简单,主要就是脚本对于相机视角的控制,具体代码如下

extends Node2D

onready var camera = $Camera2D
var scaleNum = 2
var isDrag = fasle
var startPos = Vector2.ZERO
var startCamPos = Vector2.ZERO

func _ready():
    camera.zoom = Vector2(scaleNum, scaleNum)

func _input(event):
    if event is InputEventMouseButton:
        # 缩放操作
        if event.button_index == 4:
            startPos = Vector2.ZERO
            if scaleNum <= 0.1:
                return
            elif scaleNum >= 2:
                scaleNum -= 0.3
            else:
                scaleNum -= 0.1
        elif event.button_index == 5:
            startPos = Vector2.ZERO
            if scaleNum >= 10:
                return
            if scaleNum <= 0.3:
                scaleNum += 0.03
            elif scaleNum >= 8:
                scaleNum += 0.6
            else:
                scaleNum += 0.1
        
        # 移动操作
        if event.button_index == 2 or event.button_index == 3:
            if event.is_pressed():
                isDrag = true
                startPos = event.position
                startCamPos = camera.position
            else:
                isDrag = false
                startPos = Vector2.ZERO
    
    if isDrag:
        # 只有当在非缩放才会进行移动,避免边平移边缩放导致的图片跳动
        if startPos != Vector2.ZERO:
            var offset = startPos - event.position
            camera.position = startCamPos + offset * scaleNum

func _process(delta):
    camera.zoom = lerp(camera.zoom, Vector2(scaleNum, scaleNum), 8 * delta)

实现简单的2D灯光和阴影

效果展示

创建光源

效果展示

将光源图片拖入到Light2D的texture中

效果展示

创建阴影

创建一个物体, 节点如下,使用LightOccluder2D节点产生阴影

效果展示

选择LightOccluder2D节点后点击屏幕,系统会提示创建一个多边形

效果展示

然后选择Light2D节点,选择Shadow > Enabled属性开启阴影效果

效果展示

Light2D的Shadow其他属性

  • filter: 调整阴影边缘模式
  • filter smooth:调整阴影柔滑程度,向两边延伸的程度

实现物品拖拽与放置的功能

效果图

场景搭建

创建场景,节点如下

场景图

脚本编写

godot内置了物品拖拽与放置功能,相关函数列表如下

需要注意改拖拽方法只适用于继承自Control的节点

  • get_drag_data: 当发生拖拽的时候获取的元素
  • set_drag_preview: 拖拽之后显示的图标
  • can_drop_data: 是否能够放置元素在这
  • drop_data: 当放下元素后执行的操作

创建一个脚本,全选PanelContainer并在Script属性栏中将Script设置为当前脚本

extends PanelContainer

func get_drag_data(position):
    if has_node("Sprite"):
        var texture = TextureRect.new()
        texture.texture = $Sprite.texture
        texture.rect_scale = $Sprite.scale
        set_drag_preview(texture)
        return $Sprite
    return false

func can_drop_data(position, data):
    return true

func drop_data(position, data):
    if data and !has_node("Sprite"):
        self.add_child(data.duplicate())
        data.queue_free()

实现在2D场景中显示3D物体

显示效果

场景搭建

3D物体

viewport的属性需要设置大小,以及勾选透明背景Transparent Bg, 选择RenderTarget的垂直翻转属性V Flip这样viewport获得的内容就是正常的

在2D场景中渲染3D物体核心在于使用viewport,viewport可以创建一个场景的输出映射(3D物体+camera),然后使用脚本将viewport的内容赋值给sprite即可显示出来。并且3D物体的动画也是能实时渲染到2D场景的

3D物体

创建脚本设置texture

extends Node2D

func _ready():
    var texture = $viewport.get_texture()
    $Sprite.texture = texture

使用鼠标拖拽来实现列表的滚动效果

使用鼠标拖拽来实现列表的滚动效果

场景搭建

使用鼠标拖拽来实现列表的滚动场景

scrollContainer作为滚动容器,调整container大小

为scrollContainer创建子节点GridContontainer为表格,设置columns属性为任意数字,这个示例中为3

在GridContainer下设置PanelContainer为每一个单元格,并且设置的宽高属性Min Size

元素搭建示意图

脚本编写

为ScrollContainer创建脚本,监听鼠标的点击以及拖拽事件,在信号面板中创建对应的信号gui_input

extends ScrollContainer

var isDrag = false
var startPos = 0
var dragDir = 0 # 鼠标滚动方向

func _on_ScrollContainer_gui_input(event):
    if event is InputEventMouseButton and event.is_pressed():
        isDrag = true
        startPos = event.position.y
    
    if event is InputEventMouseButton and event.is_pressed():
        isDrag = false
        startPos = 0
        # 缓动效果
        var tween = Tween.new()
        add_child(tween)
        # 属性,开始值,目标值,时间,运动曲线,缓动效果
        tween.interpolate_method(self, "set_v_scroll", self.get_v_scroll(), self.get_v_scroll() + 15 * dragDir, 0.2, Tween.TRANS_LINEAR, Tween.EASE_IN_OUT)
        # 设置回调销毁
        tween.interpolate_callback(tween, 0.2, "queue_free")
        # 开启缓动
        tween.start()
    
    if isDrag:
        var offset = event.position.y - startPos
        if offset > 0:
            dragDir = -1
        elif offset < 0:
            dragDir = 1
        self.set_v_scroll(self.get_v_scroll() - offset)
        startPos = event.position.y

现在基本功能已经完成,但是会发现当鼠标点击到GridContainer进行拖动会发现拖动事件不生效,是因为该节点在ScrollContainer的上层,他阻止了鼠标事件,需要在该节点的Mouse属性中将filter设置为Pass,这样PanelContainer以及ScrollContainer都能够监听到鼠标事件了

使用PinJoint2D实现可中心旋转的物体

实现效果

场景搭建

实现可中心旋转的物体需要用到PinJoin2D节点,该节点需要链接两个节点,一个中心点,一个旋转物体

中心点使用StaticBody2DCollisionShape2D的组合即可

旋转物体可以使用RigidBody2D, 然后创建一个Polygon2D用于绘制一个多边形

RigidBody2D可以设置以下属性

  • Mass: 可以用于控制旋转快慢,值越大转得越慢
  • Gravity Scale: 用于设置物体的重力
旋转物体

创建一个CollisionShape2D为这个多边形添加碰撞体积, 使用脚本赋值

extends RigidBody2D

func _ready():
    $CollisionPolygon2D.polygon = $Polygon2D.polygon

点击Pinjoin2D分别设置NodeA以及NodeB属性

连接节点

创建物体来触发旋转效果

随意创建一个物体,让其落到旋转物体上测试旋转效果

人物场景

VisibilityNotifier2D可以用于当物体出屏幕外之后对其进行销毁,链接screen_exited信号

extends RigidBody2D

func _on_visiblityNotifier2D_screen_exited():
    queue_free()

接下来在主场景中加载上述物体,检测到鼠标按键之后创建一个新的物体落下

extends Node2D

var cube = load("res://cube.tscn")

func _input(event):
    if event is InputEventMouseButton and event.button_index == BUTTON_LEFT and event.is_pressed():
        var c = cube.instance()
        c.position = event.position
        add_child(c)

为2D物体移动添加烟尘效果

显示效果

该文章内容主要包括场景的搭建,人物的移动,以及粒子系统实现烟雾效果

场景搭建

地板创建

地板创建

StaticBody2D是刚体节点,在其下方创建一个Polygon2D拖出一个长方形作为地板,然后创建CollisionShape2D并且大小与Polygon2D一致,为其添加碰撞属性

人物创建

人物创建

人物移动

extends KinematicBody2D

var acc = 1500
var max_speed = 400
var gravity = 800
var jump_force = 300
var fri = 10
var air_fri = 2

var move = Vector2.ZERO
var dir

func _physics_process(delta):
    if Input.is_action_pressed("ui_left"):
        dir = -1
    elfi Input.is_action_pressed("ui_right"):
        dir = 1
    else:
        dir = 0
    
    if dir != 0:
        move.x += dir * acc * delta
        move.x = clamp(move.x, -max_peed, max_speed)
        if is_on_floor():
            # 在移动并且在地面上时触发粒子
            $Particles2D.emitting = true
        else:
            $Particles2D.emitting = false
        
        # 根据移动方向设置粒子的发射角度
        if dir = 0:
            $Particles2D.rotation_degress = -25
        else:
            $Particles2D.rotation_degress = 205
    else:
        if is_on_floor():
            move.x = lerp(move.x, 0, fri*delta)
        else:
            move.x = lerp(move.x, 0, air_fri * delta)
    
    move.y += gravity * delta
    # 可以检测碰撞
    move = move_and_slde(move, Vector2.UP)

烟雾效果

创建一个粒子节点Particles2D,根据下面的属性节点调整粒子的发射

  • 创建材质: Material属性,创建了材质之后在窗口就可以发现正在发射粒子
  • 修改重力: Material.Gravity, 重力属性,有x、y、z,设置y为0,就不会粒子往下掉了,y为负数,则会往上发射
  • 修改初速度: Material.Initial Velocity, 设置为一个正数,就会往右边开始发射
  • 修改发射扩张角度: Material.Direction.Spread
  • 旋转发射方向: Transform.Rotation Degress
  • 图片素材: Texture,设置自定义的图片替换默认粒子样式,如果素材太模糊,点击素材后,选择import,设置Preset值为2D Pixel, 然后选择Reimport重新导入即可
  • 添加渐变效果,在Color属性中选择线性渐变GradientTexture并且设置透明度即可
  • 修改粒子生命周期时间: Time属性为整个存活时间,调整粒子速度: Speed Scale属性
  • 使用全局坐标计算粒子位置: Local Coords属性,点击取消,使用全局坐标计算,移动人物的时候才不会让粒子也跟着移动
  • 默认是否触发粒子: Emitting属性,将它取消勾选,然后在玩家移动时才发生粒子
粒子效果

为3D物体添加一个跟随的2D物体

实现效果

场景搭建

如图所示,创建MeshInstance节点并且分别设置一个地板以及球形3D物体

实现效果

创建camera3D并设置相机视角

为3D物体加上2D物体

在MeshInstance下新建子节点,在该例子中是创建的TextureRect节点,并设置texture图片属性

当前状态下该2D物品并没有跟随3D物体

未跟随

在3D节点下创建脚本来关联节点位置

extends MeshInstance

func _process(delta):
    var pos = get_translation()
    var screen_pos = get_parent().get_node("Camera").unproject_position(pos)
    $TextureRect.set_position(Vector2(screen_pos.x, screen_pos.y - 130))
跟随

血条的实现

效果展示

场景搭建

效果展示

只需要控制脚本,控制当前血量节点的显示与否即可

extends = HBoxContainer

var heart_full = preload("res://assets/Heart_full.png")
var heart_empty = preload("res://assets/Heart_empty.png")
var heart_half = preload("res://assets/Heart_half.png")

enum TYPES {type1, type2, type3}
export (TYPES) var type = TYPES.type1

func update_heart(value):
    match type:
        TYPES.type1:
            update_type1(value)
        TYPES.type2:
            update_type2(value)
        TYPES.type3:
            update_type3(value)
    
func update_type1(value):
    for i in self.get_child_count():
        if i < value:
            get_child(i).show()
        else:
            get_child(i).hide()

func update_type2(value):
    for i in self.get_child_count():
        if i < value:
            get_child(i).texture = heart_full
        else:
            get_child(i).texture = heart_empty

func update_type3(value):
    for i in self.get_child_count():
        if i * 2 < value - 1:
            get_child(i).texture = heart_full
        elif i * 2 == value - 1:
            get_child(i).texture = heart_half
        else:
            get_child(i).texture = heart_empty

用Light2D实现遮罩动画

实现效果

场景搭建

场景搭建

为Light2D添加亮光图片材质

设置Visibility.Light Mask, 将其全部关闭,这样灯光只会出现在logo上

场景搭建

修改Light2D的模式Mode为Mix

为TextureRect添加材质,选择Material,选择材质CanvasItemMaterial, 并且将Light Mode设置为Light Only,这样设置之后就只有被灯光照到的地方才会显示出来

场景搭建

创建动画

创建AnimationPlayer节点,新建动画

为Light2D的缩放大小创建关键帧,从而实现遮罩动画

动画

3D场景中如何控制物体的移动

自动寻路

场景搭建

自动寻路

基础3D场景的搭建

创建Navigation3D节点,然后将所有的3D场景放在这个节点下,在该节点中创建NavigationMesh,点击烘培导航地图

如果新增了3D物体,则需要重新烘培,会自动创建移动路径。只是需要注意移动的物体以及Navigation3D节点中Agent属性要与移动物体的碰撞体积的设置一致,否则会穿模

控制人物的移动

脚本如下

主脚本GameManager.gd,监听鼠标右键事件,然后将Unit移动到目标位置

extends Node3D

const _GROUND_MASK = 2

@onready var _camera: Camera3D = $Camera3D
@onready var _unit_target_location: UnitTargetLocation = $UnitTargetLocation
@onready var _unit: Unit = $Units/Unit

func _input(event):
	if event is InputEventMouseButton and event.pressed and event.button_index == MOUSE_BUTTON_RIGHT:
		var origin = _camera.project_ray_origin(event.position)
		var direction = _camera.project_ray_normal(event.position)
		var end = origin + direction * 1000
		var state = get_world_3d().direct_space_state
		var query_params = PhysicsRayQueryParameters3D.new()
		query_params.from = origin
		query_params.to = end
		query_params.collision_mask = _GROUND_MASK
		var intersection = state.intersect_ray(query_params)
		if intersection.size() > 0:
			# we hit the ground
			var pos = intersection["position"]
			_unit_target_location.click(pos)
			_unit.move_to(pos)

物体移动控制脚本Unit.gd

class_name Unit
extends CharacterBody3D

@onready var _agent: NavigationAgent3D = $NavigationAgent3D

var movement_target: Vector3:
	get:
		return _agent.target_position
	set(value):
		_agent.target_position = value

func _ready():
	# These values need to be adjusted for the actor's speed
	# and the navigation layout.
	_agent.path_desired_distance = 0.5
	_agent.target_desired_distance = 0.5
   
	# Make sure to not await during _ready.
	call_deferred("actor_setup")

func actor_setup() -> void:
	await get_tree().physics_frame
	movement_target = global_position

func move_to(pos: Vector3) -> void:
	movement_target = pos

func _physics_process(delta: float) -> void:
	if _agent.is_navigation_finished():
		return
   
	var current_agent_position = global_position
	var next_path_position = _agent.get_next_path_position()
	var new_velocity = (next_path_position - current_agent_position).normalized()
	new_velocity *= _agent.max_speed
	velocity = new_velocity
	move_and_slide()
   
	if (next_path_position - current_agent_position).length_squared() > 0.01:
		look_at(next_path_position, Vector3.UP)

目标点光标控制脚本

class_name UnitTargetLocation
extends Node3D

@onready var _anim: AnimationPlayer = $AnimationPlayer

func _ready():
   # initialize to transparent colors everywhere
   var m: ShaderMaterial = $Mesh.get_active_material(0)
   m.set_shader_parameter("radius", 0.01)

func click(position: Vector3):
   global_position = position + Vector3.UP * 0.02
   _anim.play("UnitTargetLocation")

3D场景中物体的自动寻路

自动寻路

场景搭建

自动寻路

上述BASE节点下分别为相机、灯光、与地板

在地板中添加材质, 设置路径Mesh属性 -> Material

[gd_resource type="StandardMaterial3D" load_steps=2 format=2]

[ext_resource path="res://.../grid.png" type="Texture2D" id=1]

[resource]
albedo_texture = ExtResource( 1 )
uv1_scale = Vector3( 10, 10, 1 )

然后创建Path3D以及PathFollow3D以及角色模型(一个简单的柱体和球体)

3D角色

角色模型

材质编写

分别在Body、Head中加入材质,设置Geometry属性,创建tres文件, 这里将物体控制为红色

[gd_resource type="StandardMaterial3D" format=2]

[resource]
albedo_color = Color( 1, 0, 0, 1 )

运动控制

Path3D的属性面板中,新建Curve3D,然后在主窗口上方的菜单栏选择路径节点创建按钮,绘制运动路径

路径创建

最后在character上创建运动脚本,根据父节点中的运动路径创建一个位置列表,然后定时的将人物移动到该节点位置

extends Node

@export var waypoint_check_distance: float = 0.1
@export var total_loop_time: float = 5.0

var _path_follow: PathFollow3D
var _waypoint_timer: Timer
var _waypoint_positions: PackedVector3Array
var _current_path_time: float
var _current_waypoint_index: int
var _next_waypoint_index: int
var _moving: bool

func _ready() -> void:
	_path_follow = get_parent() as PathFollow3D
	_waypoint_timer = get_node("WaypointTimer") as Timer
	
	var path_curve: Curve3D = _path_follow.get_parent().curve
	var n_waypoints: int = path_curve.get_point_count()
	_waypoint_positions = PackedVector3Array()
	for i in range(n_waypoints - 1):
		_waypoint_positions.append(path_curve.get_point_position(i))
	
	_current_waypoint_index = 0
	_next_waypoint_index = 1
	_moving = true

func _process(delta: float) -> void:
	if _moving:
		_current_path_time += delta
		_path_follow.progress_ratio = _current_path_time / total_loop_time
		
		var d = (_waypoint_positions[_next_waypoint_index] - _path_follow.position).length()
		if d < waypoint_check_distance:
			_current_waypoint_index = _next_waypoint_index
			_next_waypoint_index = (_current_waypoint_index + 1) % _waypoint_positions.size()
			_waypoint_timer.start()
			_moving = false

func _on_waypoint_timer_timeout() -> void:
	_moving = true

3D基础场景的搭建

3D场景

在3D场景中,比较常用到的是RigidBody3D以及CollisionShape3DMesh节点,作用如下

RigidBody3D节点

  • 它是一种3D物理体,用于模拟刚体物体在物理世界中的行为。
  • 可以对其应用力、阻力、冲量等,使其受到重力、碰撞等物理效果的影响。
  • 通常用于模拟掉落、弹跳、滚动等情况,为游戏增加真实感。

StaticBody3D节点

  • StaticBody3D可以与其他刚体(RigidBody)或运动体(CharacterBody)进行碰撞检测和响应。
  • 自身不会受到物理作用力的影响,保持静止状态。其他物体在与它碰撞时,会产生相应的物理反应,如弹rebond和滚动等。
  • 通常用于表示地形、建筑物、障碍物等静态元素。
  • 由于StaticBody3D不需要计算运动学方程,所以相比RigidBody更高效,在需要大量静态物体的场景中,使用StaticBody3D可以减少CPU计算开销。

CollisionShape3D节点

  • 它定义了一个3D碰撞体的形状,通常附加在RigidBody3D等物理对象上。
  • 可以设置为各种形状,如球体、盒体、胶囊体等,用于检测碰撞。
  • 确保物体在物理模拟中正确地相互作用和响应碰撞。

Mesh节点

  • 它用于渲染3D模型网格,决定了3D对象在场景中的可视外观。
  • 可以加载各种3D模型文件(.obj、.fbx等),也可以通过代码程序动态生成。
  • 通过设置材质、纹理等,可以控制Mesh的渲染效果。

场景搭建

3D场景

基础场景

这里的地板使用的是staticbody3D节点,用于设置与球体的碰撞

Physics Material Override属性中新建一个tres文件,并且设置弹力为0.7(值越大弹力越大)

创建好地板与灯光,然后创建球体,运行就能发现所有的物理属性都存在了,重力,碰撞,弹力等

球体创建

3D场景

设置MeshInstance3D的Mesh属性为sphereshape3D,并且在下方的Surface Material Override中新建StandardMaterial3Dtres, 并且在对应的Albedo中设置物体颜色

设置CollisionShape3D的Shape属性,为sphereshape属性,为节点添加碰撞体

如何将godot3项目转为godot4

  • onready修改为@onready
  • export修改为@export
# 如果变量后面是 := 那么就需要定义get或者set,如果是 = 就不需要定义
@export var detail := 0  :
	set = set_tileset
    get = get_tileset
  • 枚举值修改为@export_enum(...) var v: String = ""
  • tween节点使用create_tween()进行创建
  • yield改为await
  • 信号的连接改为signala.connect(funca)
  • 时间的获取
# 修改前
var d = OS.get_datetime()
d.erase("dst")
var s = ""

for i in (d.values()):
    s += str(i) + " "

# 修改后
var time = Time.get_datetime_dict_from_system()
time.erase("dst") # 删除夏令时标志

var datetime_string := ""
for value in time.values():
    datetime_string += str(value) + " "

print(datetime_string)
  • Engine相关

# 修改前
func set_iterations(arg := iterations):
	iterations = max(1, arg)
	Engine.iterations_per_second = iterations

func set_target_fps(arg := target_fps):
	target_fps = abs(arg)
	#print("target_fps: ", target_fps)
	Engine.target_fps = target_fps
    
# 修改后
func set_iterations(arg := iterations):
    iterations = max(1, arg)
    ProjectSettings.set_setting("physics/iterations_per_second", iterations)

func set_target_fps(arg := target_fps):
    target_fps = abs(arg)
    ProjectSettings.set_setting("display/window/target_fps", target_fps)
  • MainLoop.NOTIFICATION_WM_QUIT_REQUEST修改为``
  • 文件操作相关, File修改为FileAccess, Dir修改为DirAccess
  • PoolVector2Array修改为PackedVector2Array
  • update()修改为queue_redraw()方法,重新绘制
  • deg2rad修改为deg_to_rad
  • color.white修改修改为color.WHITE,所有颜色名称都变成大写了
  • extends VisibilityNotifier2D修改为extends VisibleOnScreenNotifier2D
  • Engine.editor_hint修改为Engine.is_editor_hint()
  • KinematicBody2D修改为CharacterBody2D
  • Sprite修改为Sprite2D
  • stepify修改为snapped
  • InputMap.get_action_list修改为InputMap.action_get_events
  • set_shader_param修改为set_shader_parameter

使用shader实现屏幕震动效果

shader屏幕震动效果示例

代码详解

shader_type canvas_item; # 着色器类型
uniform float ShakeStrength = 0; # 用于控制抖动强度。初始值为0,即不抖动。
uniform vec2 FactorA  = vec2(100.0,100.0); # 影响抖动的频率
uniform vec2 FactorB  = vec2(1.0,1.0); # 影响抖动的相位
uniform vec2 magnitude = vec2(0.01,0.01); # 用于控制抖动的幅度
uniform sampler2D SCREEN_TEXTURE : hint_screen_texture, filter_linear_mipmap; # 定义了一个统一采样器。hint_screen_texture指定这个纹理是屏幕纹理。

# 像素着色器的主要函数
void fragment() {
    vec2 uv = SCREEN_UV; # 获取当前像素的UV纹理坐标
	uv -= 0.5;
    uv *= 1.0 - 2.0 * magnitude.x;
    uv += 0.5; # 这些操作将UV坐标从[0,1]范围映射到[-magnitude.x, 1+magnitude.x]范围。
	vec2 dt = vec2(0.0, 0.0); # 初始化偏移量向量
	dt.x = sin(TIME * FactorA.x+FactorB.x) * magnitude.x; # 计算X方向的偏移量。
	dt.y = cos(TIME *FactorA.y+ FactorB.y) * magnitude.y; # 计算Y方向的偏移量。
	COLOR = texture(SCREEN_TEXTURE,uv + (dt*ShakeStrength)); # 从屏幕纹理中采样颜色,采样的UV坐标为原始UV坐标加上由抖动偏移量和抖动强度计算得到的偏移。
}

实现抽奖功能

UI拆解

分为装饰性以及功能性UI两种,都是比较容易绘制的

直接截图并对其进行临摹,这里就不展示具体的绘制过程了,下面主要讲解功能性代码的实现

功能实现

大概流程如下:

  • 转盘包含8个内容,这种奖励都是固定的,可以直接固定

  • 点击开始抽奖按钮

  • 播放转盘转动动画

  • 使用一个随机函数确定最终奖励位置

  • 获得奖励并且提示

extends Node2D

@onready var board: AnimatedSprite2D = $board

var curr_frame = -1

func _ready() -> void:
	pass

func _process(delta: float) -> void:
	if curr_frame == -1 or not board.is_playing():
		return
	
	if board.frame == curr_frame:
		print("抽奖完成")
		curr_frame = -1
		board.pause()


func _on_draw_pressed() -> void:
	board.frame = 0
	board.play("default")
	await get_tree().create_timer(1.5).timeout
	curr_frame = randi_range(0, 7)

核心创意三要素

核心创意三要素

主题

以什么题材来做游戏

“比方说“在天上飞让人觉得舒服”,这里“飞行”就是主题。以此为例,请各位通过“飞行”二字进行联想,把联想到的事物尽可能多地罗列出来。喷气机、鸟、热气球、气球、跳伞、魔女的扫帚、直升机、魔毯、天使、苍蝇、飘落的花瓣、蝴蝶……相信各位和我一样,能联想到很多东西,而且不同人想到的东西也不一样。对于“飞行”,人们很难有一个统一的印象。

但是“飞行”一词用作主题太过宽泛,需要进一步缩小范围。缩小范围的方法有很多,可以是“喷气机”“鸟”等具体事物,可以是“滑翔伞”“跳台滑雪”等竞技项目,可以是“连续按键”“触摸手机屏幕”等操作,甚至可以是“拍打翅膀”“飘浮”等动词。

总之,一切事物皆可成为游戏的主题。不过,单有主题并不能构成创意,它还需要另一个要素——概念。”

概念

概念是指让玩家玩什么

“概念明确了主题中(或是与主题相关联的元素中)哪一部分是“拿来给玩家玩的”,简而言之就是对“这是一款玩什么的游戏”的一个定义。比如要为玩家提供什么样的体验,带来怎样的游戏感受,产生何种情感共鸣等。”

“现在假设主题为“棒球”。选定概念,就是从棒球的大量趣味元素中选出一部分进行重点渲染。它可以是击球,可以是投球,可以是防守,甚至可以淡化选手这一方面,让玩家当教练坐镇指挥。着眼点不同,概念也会有很大不同。 “挖掘”“吃”等动词类主题也是一样,我们要考虑将该动作的某一阶段拿出来着重刻画。以“挖掘”为例,刻画大刀阔斧穿山钻地的畅快感与刻画危矿中如履薄冰的紧张感就是两种完全不同的概念。”

“还要注意,单有抽象的语言并不能构成概念。就像单凭“飞行”和“让玩家享受在空中自由飞行的感觉”这一对主题与概念无法构成游戏一样。 空中飞行舒服在哪里?“悬空感”“俯瞰风景”“俯冲”“迎合气流而动”“巧妙运用翅膀”“超音速飞行”都是切入点。这些切入点用作创意时一定要足够新颖才行。”

系统

如何频繁呈现出好玩的内容

“一个好的创意,应该让玩家随着一次次“舒服”的体验不断接近游戏目标,所以我们需要在游戏中不断触发这种体验。”

  • 什么样的机制或规则能实现这一目的
  • 什么样的操作方式能实现这一目的
  • 什么样的表现手法能实现这一目的

“只有能稳定带来概念中所述的体验,且能频繁触发该体验以达到概念所追求之效果的实现手段(机制、操作方式、表现手法等),才称得上合格的系统。”

核心创意思路

核心创意的思路
  • 不以现有游戏为原型
  • 不以类型为出发点
  • 先在脑子里跑跑看
  • 一个创意不要思考太久
  • 创意是否会带来全新体验
  • 逆向思维
  • 有些创意必须有视觉效果支持

所有创意都是为了实现概念服务的

开工之前需要考虑单凭手中的创意能够作出一款游戏吗。“一款拿来卖的游戏产品需要更长的游戏寿命。这就要求有更多的创意,也就是要以核心创意为核心,再添加多个扩充核心的创意。”

如果一个创意只能派生出一两个创意,表明它不足以成为核心创意。好的创意能够让听者立刻在脑海中产生画面与节奏,接着大量创意就会如泉涌般随之而来。

核心创意太弱会导致创意内容不足,使得一款游戏存在多个小核心。然后每个小核心又会派生一些创意来扩充它们自己,到头来游戏就会成为一盘散沙

如何考虑游戏节奏

核心创意与节奏的关系
  • 最合适的节奏
  • 节奏的关键元素
  • 操作感与节奏
  • 任何创意都有它的节奏

示例

“游戏的主人公只能向上爬一个方块,所以遇到上图的情况时完全无法继续前进,也就是我们俗称的“卡死了”。要知道,这款游戏起初是面向街机开发的,玩家花钱投了币却遇到“卡死”,这对游戏而言是致命的。最终铁块的设计被放弃,取而代之的是×标方块。×标方块需要挖掘 5 次才能打穿。然而问题又来了,对于手速足够快的玩家来说,挖 1 次和挖 5 次在时间上没有太大区别。

于是创作者又给×标方块添加了“挖穿后消耗 20% 氧气”的机制。这让玩家开始衡量挖穿(消耗 20% 氧气)与绕路(被压死)的风险,使得更多人选择绕开×标方块掘进。

该游戏第一个试玩版本只有一个无尽关卡,玩家要用三次机会挑战深度极限。后来创作者发现一直保持紧张感会使游戏变得严肃,中间夹杂些喘息的空间“反而更让人舒服,便在每 100 米设置了一个中断点。

中断点的设计给游戏创造了节奏,使玩的过程有张有弛,还让玩家有了一步步前进的成就感。此外,抵达中断点的瞬间,头顶上所有方块会被一次性消除,所以当玩家面临大量崩落的方块时,只要能在被压死前冲到 100 米处,就能化险为夷。这又一次扩充了游戏的不确定性,使玩家在游戏中产生诸如“最后关头居然挺过来了!”“可惜差那么一点!”的喜怒哀乐。”

游戏设计的基本原理

游戏设计基本原理

游戏创新原理

  • 游戏的对称性/非对称性和同步性
  • A最大,鬼万能
  • 巴特尔的玩家分类理论
  • 合作与对抗
  • 公平
  • 反馈循环
  • 加德纳的多元智能理论
  • 霍华德的隐匿性游戏设计法则
  • 信息
  • 科斯特的游戏理论
  • 拉扎罗的四种关键趣味元素
  • 魔法圈
  • 采取行动
  • MDA:游戏的机制、运行和体验
  • 记忆和技巧
  • 极小极大和极大极小
  • 纳什均衡
  • 帕累托最优
  • 得益
  • 囚徒困境
  • 解谜游戏的设计
  • 石头剪刀布
  • 7种通用情感
  • 斯金纳箱
  • 社会关系
  • 公地悲剧
  • 信息透明
  • 范登伯格的大五人格游戏理论
  • 志愿者困境

游戏创作原理

  • 80/20法则
  • 头脑风暴的方法
  • 消费者剩余
  • 核心游戏循环
  • 定义问题
  • 委员会设计
  • 环境叙事
  • 体验设计
  • 心流
  • 4种创意方法
  • 游戏体裁
  • 游戏的核心
  • 游戏中的“约定俗成” 原理43 格式塔
  • 补充规则
  • 迭代
  • 魔杖
  • 超游戏思维
  • 对象,属性,状态
  • 吸引注意力的方法
  • 纸上原型
  • 三选二:快速,便宜,优质 原理52 游戏测试
  • 解决问题的障碍
  • 原型
  • 风险评估
  • 供需关系
  • 协同效应
  • 主题
  • 时间和金钱
  • 以用户为中心的设计 原理61 路径指示

游戏平衡原理

  • 成瘾途径
  • 注意与感知
  • 平衡和调试
  • 细节
  • 加倍和减半
  • 规模经济
  • 玩家的错误
  • 不被惩罚的错误 原理70 希克定律
  • 兴趣曲线
  • 学习曲线
  • 损失规避
  • 马斯洛需求层次理论 原理75 最小/最大化
  • 惩罚 原理77 沙盒与导轨 原理78 持续注意力 原理79 可变奖励

解决问题原理

  • 先行组织者
  • 功能可见性暗示
  • 巴斯特原则
  • 认知偏差
  • 占优策略
  • 菲兹定律
  • 基本归因错误
  • 黄金比例
  • 破坏者
  • 前期宣传
  • 即时满足与延迟满足
  • 别让我思考——克鲁克的可用性第一定律 原理92 音乐与多巴胺
  • 节奏
  • 解决问题的方法
  • 满意与优化
  • 成就感
  • 空间感知
  • 时间膨胀
  • 工作记忆
  • 零和博弈

讲解shader原理,以及shader实战

TODO

TODO

TODO

TODO

TODO

TODO

TODO