开始制作一个游戏

2021-06-06 约 5687 字 预计阅读 12 分钟

这里没有以官方的项目进行开发,而是选择youtube上的一个教程,ARPG的制作来进行学习,因为这个更符合我要制作的rpg游戏类型,并且也是pixel风格的

1. 项目初始化

1.1. 新建项目

这个没什么好说的,步骤几乎没有

1.2. 导入资源

在左侧按照自己的习惯建立好文件夹以后,将文件直接拖拽进来即可, 需要注意的地方是,如果是导入pixel art的资源,为了防止godot进行处理,需要把import选项里的滤镜filter关闭掉,否则像素会被重新处理

image-20210627115802890

注意: 这一步的设置非常重要,如果说在运行的时候在大尺寸屏幕下发现物体的图像有些模糊,

记得用2d pixel的预置重新导入(reimport)

1.3. 建立场景

每个项目至少有一个根场景

1.4. 基本设置

1.4.1. 设置运行入口场景

运行的时候必须有个主场景,可以在project settings里进行设置

image-20210627120041659

1.4.2. 设置游戏窗口大小

像素游戏可以设置的小一点,比如320x180

image-20210627130746240

1.4.3. 设置测试运行窗口大小

同时我们可以设置测试的窗口大小

image-20210628163929714

1.4.4. 设置拉伸

为了保持开发的时候像素贴图和屏幕的比例, 即便我们设置了测试窗口的大小,我们依然可以通过拉伸保持比例,把stretch改成2d

image-20210628164205941

1.4.5. 编辑器的自定义设置

编辑器也可以进行一些自定义的设置,比如顶部在制作2D的时候不需要Assets和3D,我们可以新建一个profile,然后应用它

image-20210628171552924

1.4.6. 编辑习惯的设置

我个人习惯把tab自动设置成4个space

image-20210628172307388

2. 添加Sprite

在根节点上点加号添加一个Kinematicbody

image-20210628164516791

刚添加完是看不见的,我们需要在Player的下面加上一个sprite

image-20210628164756393

2.1. 设置角色贴图

点击sprite,右侧属性里有texture,可以在资源栏里将角色贴图拖进去

刚添加进去是这样的

image-20210628165040094

我们需要将动画的帧属性进行设置

image-20210628165158665

设置好以后就变成了一个单一的图像,从frame设置可以设置显示那一帧,也可以用坐标来进行选择frameframe coords只能用一个,结果是一样的并且会自动转换(注意这里的坐标是值帧的坐标,比如帧是4x4的,那么x为2,y为0就是指第一排的第三帧)

为了防止我们在移动角色的位置的时候,sprite和Kinematicbody分离,可以将Kinematicbody的子节点全部和自己锁定,一起移动

image-20210628170209972

2.2. 添加角色的GDScript

选择角色根节点,点击右侧的添加脚本的按钮在合适的文件夹添加脚本.

修改一下进行测试

1
2
3
4
5
6
7
8
extends KinematicBody2D


func _ready():

	print("hello!!!!")


测试运行就可以在控制台看见输出了

2.3. 编写控制移动脚本

第一个版本, 根据按键移动, 直接修改坐标

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
func _physics_process(delta):

    if Input.is_action_pressed("ui_right"):
        position.x += 10
    elif Input.is_action_pressed("ui_left"):
        position.x -= 10
    elif Input.is_action_pressed("ui_down"):
        position.y += 10
    elif Input.is_action_pressed("ui_up"):
        position.y -= 10

第二个版本 利用move_and_collide来按照向量移动

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
func _physics_process(delta):
    
    if Input.is_action_pressed("ui_right"):
        velocity.x = 4
    elif Input.is_action_pressed("ui_left"):
        velocity.x = -4
    elif Input.is_action_pressed("ui_down"):
        velocity.y = 4
    elif Input.is_action_pressed("ui_up"):
        velocity.y = -4
    else:
        velocity =Vector2.ZERO
    move_and_collide(velocity)

第三个版本 优化控制器的代码用Input.get_action_strength简化

1
2
3
4
5
6
7
8
9
var acceleration = 0
var velocity = Vector2.ZERO

func _physics_process(delta):    
    var input_vector = Vector2.ZERO
    input_vector.x = Input.get_action_strength("ui_right") - Input.get_action_strength("ui_left")
    input_vector.y = Input.get_action_strength("ui_down") - Input.get_action_strength("ui_up")
    velocity = input_vector
    move_and_collide(velocity * delta * 100)

这里原视频教程里乘了一次delta,我觉得没有必要, 因为_physics_process本来就是和真实世界时间同步的,所以如果

第四个版本 增加加速度和摩擦力, 为了让物体运动看起来更真实,有一个加速和减速的过程.

我的思路是:

  • input_vector就是最大速度, 比如摇杆有轻重之分,所以摇杆未达到一个端点的时候最大速度是不可能超过端点的最大速度
  • 使用_physics_process的时候帧率是一定的, 运行距离 = 速度 x delta
  • 如果是匀加速运动,加上加速度以后: 运行距离 = (速度 + 加速度) x delta, 满足条件,速度是不断变化的
  • 速度最大值不能超过摇杆的力度, 所以可以将input_vector的向量作为一个系数来获取速度最大值
  • 假定一个速度, 在320x180像素的屏幕上, 物体用最大速度移动完一个横屏需要4秒,那么最大速度为80
  • 当物体改变一个轴上的方向之后,加速度要快速回到0重新计算,所以这个加速度是比较大的,否则就有一个转身的时间,这个在RPG和ARPG里会显得不流畅,尽量把这转身加速度的值设置的很大
  • 当某个轴上的摇杆力度为0的时候,没有转身,产生的是摩擦力, 这个时候有个摩擦力加速度,这个加速度也可以很大,比转身的略小一些
  • 如果是正在转身(速度没有到达0或者和方向和控制方向不相同),那么这个轴上的方向回到0以后将转身的速度到0以后才启用加速度,否则
  • 摇杆没有按满的情况下是有一定系数的(0-1),这个系数要用来限制最终速度

在计算加速度的时候有地方要注意,加速度是以秒为单位,在physcis_process里经过的时间不是秒,所以加速度要乘以delta,才能成为当前准确的加速度

目前的逻辑如下,可以继续改进

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
extends KinematicBody2D

# 这里所有速度是秒为单位的
const ACCELERATION = 800                 # 正向动加速度
const FRICTION_ACCELERATION = 900        # 摩擦加速度
const TURNBACK_ACCELERATION = 1000        # 转身加速度
const MAX_SPEED = 200                    # 最大速度
# 当前速度
var velocity = Vector2.ZERO

func _physics_process(delta):
    var input_vector = Vector2.ZERO
    input_vector.x = Input.get_action_strength("ui_right") - Input.get_action_strength("ui_left")
    input_vector.y = Input.get_action_strength("ui_down") - Input.get_action_strength("ui_up")
    # 计算速度
    cal_speed(input_vector, delta)
    # 计算移动距离
    var move_velocity = velocity * delta
    # 控制器在轴移动的时候距离进行控制在一个圆形范围内
    move_velocity = move_velocity.normalized()
#    if abs(move_velocity.x) !=0 and abs(move_velocity.y) !=0:
#        print(move_velocity)
    move_and_collide(move_velocity)

# 获取速度 两个轴分开计算速度
# 判断使用摩擦力,转向,还是正向加速度
# 如果目前的方向是0 启用摩擦力 这个是优先级最高的
# 判断转向的方式为: 目前速度的方向和摇杆的方向不一致
# 目前移动方向和控制方向不一致, 速度和控制方向一致
func cal_speed(strength, delta):
    var speed_x = get_axis_speed(strength.x, velocity.x, delta)
    var speed_y = get_axis_speed(strength.y, velocity.y, delta)
    # 根据摇杆强度,进行系数计算,这样在不同压力下最终得到的速度是不一样的
    velocity.x = speed_x * abs(strength.x)
    velocity.y = speed_y * abs(strength.y)

# 获取单个轴的速度
func get_axis_speed(axis_strength, axis_current_velocity, delta):
    var input_sign = sign(axis_strength)
    var current_velocity_sign = sign(axis_current_velocity)
#    if abs(velocity.x) !=0:
#        print("input_sign", input_sign)
#        print("current_velocity_sign", current_velocity_sign)
    var acceleration = 0
    # 控制器为0 启用摩擦力 优先级最高
    if abs(axis_strength) == 0:
        if axis_current_velocity != 0:
            # 反向加速度 和速度方向相反
            acceleration = FRICTION_ACCELERATION * current_velocity_sign * -1
        else:
            acceleration = 0
    # 控制器不为0
    else:
        # 当前静止
        if axis_current_velocity == 0:
            acceleration = ACCELERATION * input_sign
        # 当前在移动中
        else:
            # 如果控制器方向和移动同向, 使用普通加速度
            if input_sign == current_velocity_sign:
                acceleration = ACCELERATION * input_sign
            # 如果控制器方向和移动不同向, 使用转身加速度
            else:
                acceleration = TURNBACK_ACCELERATION * input_sign
    var axis_speed = axis_current_velocity + acceleration * delta
    # 速度的最大值不能超过最大值, 最小值为0,因为从0开始就要重新计算加速度了
    if abs(axis_speed) >= MAX_SPEED:
        # 保证方向不能变
        axis_speed = sign(axis_speed) * MAX_SPEED
    # 如果计算后的速度方向和原方向不同,说明转身已经完成, 速度归零
    if axis_current_velocity != 0 and current_velocity_sign != sign(axis_speed):
        axis_speed = 0
    # 结合控制器的强度系数最大为1,最小为0 (这一步在最后做)
    # axis_speed = axis_speed * abs(axis_strength)
    return axis_speed

第五个版本 利用新版本的函数优化代码,进行简化, 特别是转身的判断加速度的正负值计算容易出错

新版本有两个函数move_towardclamped(不需要了)

move_toward可以用来实现加速度, clamped可以用来限制最大速度.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
extends KinematicBody2D

# 这里所有速度是秒为单位的 所以在进行计算的时候要乘以delta
const ACCELERATION = 10                 # 正向动加速度
const FRICTION_ACCELERATION = 10        # 摩擦加速度
const TURNBACK_ACCELERATION = 15        # 转身加速度
const MAX_SPEED = 200                    # 最大速度
# 当前速度 这个是和delta一致的
var velocity = Vector2.ZERO



func _physics_process(delta):
    var input_vector = Vector2.ZERO
    input_vector.x = Input.get_action_strength("ui_right") - Input.get_action_strength("ui_left")
    input_vector.y = Input.get_action_strength("ui_down") - Input.get_action_strength("ui_up")
    # 控制器在轴移动的时候距离进行控制在一个圆形范围内
    # 这里的iput当做一个系数0-1, 在normalized的作用下,无论什么方向都会过1
    input_vector = input_vector.normalized()
    velocity.x = move_towards_axis(input_vector.x, velocity.x, delta)
    velocity.y = move_towards_axis(input_vector.y, velocity.y, delta)
    move_and_collide(velocity)
    
    
#    print("move_x_speed:", move_x_speed, "velocity:", velocity) 

func move_towards_axis(input_axis, velocity_axis, delta):
    # 这里两个轴还是分开来计算
    var towards = calculate_speed(input_axis, velocity_axis, delta)
    # 摇杆压力系数决定最大速度, 加速度不变
    var move_axis_velocity = move_toward(towards["speed"], towards["target"] * delta * abs(input_axis), towards["acceleration"] * delta)
    return move_axis_velocity


# 因为是用move_toward来进行步进, 我们需要得出的是使用什么加速度以及目标速度
func calculate_speed(input, current_speed, delta):
    var towards = {"target": 0, "speed": 0 ,"acceleration": 0}
    var direction_input = sign(input)
    var direction_current_speed = sign(current_speed)
    # 分3种情况
    # 第一种 从静止到启动(单轴或多轴) 同向加速(单轴或多轴)
    if abs(direction_current_speed) == 0:
        # 当前速度为0
        if abs(direction_input) == 0:
            # 从静止到静止
            towards["target"] = 0
            towards["acceleration"] = 0
            towards["speed"] = 0
        else:
            # 从静止开始加速
            towards["target"] = MAX_SPEED * direction_input
            towards["acceleration"] = ACCELERATION
            towards["speed"] = 0
    else:
        if abs(direction_input) == 0:
            # 第二种 某个轴开始静止
            # 从运动到静止
            towards["target"] = 0
            towards["acceleration"] = FRICTION_ACCELERATION
            towards["speed"] = abs(current_speed) * direction_current_speed
        else:
            # 运动中的变化
            if direction_current_speed == direction_input:
                # 同向
                towards["target"] = MAX_SPEED * direction_current_speed
                towards["acceleration"] = ACCELERATION
                towards["speed"] = abs(current_speed) * direction_current_speed
            else:
                # 反向
                # 第三种 某个轴开始反向, 需要用转向加速度, 和摩擦基本相同, 归零即可
                towards["target"] = 0
                towards["acceleration"] = TURNBACK_ACCELERATION
                towards["speed"] = abs(current_speed) * direction_current_speed
    return towards



第六个版本 用move_and_slide替代move_and_collide

为了使边缘碰撞更加平滑,改用move_and_slide来进行移动, 需要改动的地方是move_and_colide是自己处理delta的,无需在代码中再次乘以delta

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
extends KinematicBody2D

# 这里所有速度是秒为单位的 所以在进行计算的时候如果是用move_and_collide要乘以delta,如果是move_and_slide则不需要
const ACCELERATION = 800                  # 正向动加速度
const FRICTION_ACCELERATION = 1000        # 摩擦加速度
const TURNBACK_ACCELERATION = 1200        # 转身加速度
const MAX_SPEED = 200                     # 最大速度
# 当前速度 这个是和delta一致的
var velocity = Vector2.ZERO



func _physics_process(delta):
    var input_vector = Vector2.ZERO
    input_vector.x = Input.get_action_strength("ui_right") - Input.get_action_strength("ui_left")
    input_vector.y = Input.get_action_strength("ui_down") - Input.get_action_strength("ui_up")
    # 控制器在轴移动的时候距离进行控制在一个圆形范围内
    input_vector = input_vector.normalized()
    velocity.x = move_towards_axis(input_vector.x, velocity.x, delta)
    velocity.y = move_towards_axis(input_vector.y, velocity.y, delta)
    # 如果发生碰撞velocity会被改变,所以这里要重新赋值
    velocity = move_and_slide(velocity)


func move_towards_axis(input_axis, velocity_axis, delta):
    # 这里两个轴还是分开来计算
    var towards = calculate_speed(input_axis, velocity_axis)
    # 摇杆压力系数决定最大速度, 加速度不变
    # 这里的iput当做一个系数0-1, 在normalized的作用下,无论什么方向都会过1
    # 注意delta在不同的移动方法的使用move_and_slide和move_and_colide不一样
    var move_axis_velocity = move_toward(towards["speed"],
                                        towards["target"] * abs(input_axis), # * delta,
                                        towards["acceleration"] * delta
                                        )
    return move_axis_velocity


# 因为是用move_toward来进行步进, 我们需要得出的是使用什么加速度以及目标速度
func calculate_speed(input, current_speed):
    var towards = {"target": 0, "speed": 0 ,"acceleration": 0}
    var direction_input = sign(input)
    var direction_current_speed = sign(current_speed)
    # 分3种情况
    # 第一种 从静止到启动(单轴或多轴) 同向加速(单轴或多轴)
    if abs(direction_current_speed) == 0:
        # 当前速度为0
        if abs(direction_input) == 0:
            # 从静止到静止
            towards["target"] = 0
            towards["acceleration"] = 0
            towards["speed"] = 0
        else:
            # 从静止开始加速
            towards["target"] = MAX_SPEED * direction_input
            towards["acceleration"] = ACCELERATION
            towards["speed"] = 0
    else:
        if abs(direction_input) == 0:
            # 第二种 某个轴开始静止
            # 从运动到静止
            towards["target"] = 0
            towards["acceleration"] = FRICTION_ACCELERATION
            towards["speed"] = abs(current_speed) * direction_current_speed
        else:
            # 运动中的变化
            if direction_current_speed == direction_input:
                # 同向
                towards["target"] = MAX_SPEED * direction_current_speed
                towards["acceleration"] = ACCELERATION
                towards["speed"] = abs(current_speed) * direction_current_speed
            else:
                # 反向
                # 第三种 某个轴开始反向, 需要用转向加速度, 和摩擦基本相同, 归零即可
                towards["target"] = 0
                towards["acceleration"] = TURNBACK_ACCELERATION
                towards["speed"] = abs(current_speed) * direction_current_speed
    return towards



目前这个版本才算是比较完美的版本了,特征如下

  • 平滑碰撞(可以自定义摩擦系数)
  • 正向,反向,停止, 3种加速度可以自动调节
  • 加速度和速度的定义是以秒为单位的,方便修改和理解
  • 每个轴的不同加速度的使用是不会相互受影响的

2.4. 添加碰撞

在角色的父节点下,添加一个CollisionShape2D,然后在右侧inspector里的shape选择一个形状,并进行修改

为了让角色遇到碰撞时边缘移动平滑,改用move_and_slide函数,注意slide,不需要乘以delta,所以样重新计算下velocity

1
move_and_slide(velocity / delta)

2.5. 将角色保存为场景

为了复用角色,可以将角色保存为场景,右键节点找到save branch as scene,保存可以看到保存后的角色多了一个图标

image-20210630170305903

然后还有一个小操作,将角色放在场景的起始点(0,0),可以在场景里选中角色,在transform里修改x和y的值为0

3. 制作场景中的静态物体

3.1. 制作树

第一步

新建一个场景,选择staticBody2D作为根节点,然后添加Sprite子节点,并将树的贴图拖到右侧texture里,然后保存场景

第二步

添加CollisionShape2D节点,并修改图形

4. 场景中的排序

这个直接影响了各个场景或者节点之间在发生重叠的时候,谁显示在谁的上面

先把根节点改成ysort类型,右键->change type -> ysort

然后将各个节点或者场景的坐标进行修改

将sprite和collisionshape2d一起拖动即可

image-20210630174753294

image-20210630174822259

image-20210630174838801

这样就保证了在运行的时候,当人物在树下面的时候,y比bush的y大,所以bush被遮挡,当人在树上面的时候(去除碰撞体积), 人物被遮挡

image-20210630175018896

image-20210630175043821

5. 基本的角色动画制作

5.1. 创建动画节点

在player的场景里的根节点下添加AnimationPlayer节点

image-20210703081944098

5.2. 新建一个动画

在下面的animation工具栏创建新的动画 Animation->New

image-20210703082022479

5.3. 修改动画属性-循环 时长 间隔 起始位置

打开循环,修改总时长和时间间隔, 这里设置的0.4秒一个动画,间隔0.1秒,一共4帧

image-20210703082123965

5.4. 添加帧

到sprite的属性栏找到animation的frame,点击加好添加到动画的帧里

image-20210703082243605

5.5. 设置自动播放

如果需要可以将某个动画设置为自动播放

image-20210703082534684

在脚本里控制动画

获取节点

在脚本里,我们可以很轻易的获取节点

1
2
3
4
var playerAnimation = null

func _ready():
    playerAnimation = get_node("AnimationPlayer")

操作动画

最简单方式我们可以这么写

1
2
3
4
5
6
7
8
func _physics_process(delta):
    var input_vector = Vector2.ZERO
    input_vector.x = Input.get_action_strength("ui_right") - Input.get_action_strength("ui_left")
    input_vector.y = Input.get_action_strength("ui_down") - Input.get_action_strength("ui_up")
    if input_vector.x > 0:
        playerAnimation.play("RunRight")
    else:
        playerAnimation.play("RunLeft")

但是这样太复杂了,有更好的选择就是用AnimationTree

在根节点上添加一个AnimationTree,然后在右侧Anim Player选择刚才制作的动画节点,勾选激活,并将tree root选择AnimationNodeStateMachine

image-20210703101439337

然后在根节点上添加一个BlendSpace2D

image-20210703101715866

然后点击箭头切换模式为选择模式,然后修改名称,

image-20210703101906645

点击那个编辑按钮可以打开一个新的窗口

image-20210703102023469

在4个顶点添加动画, 分别是IdleLeft IdleDown IdleRight IdleUp(从左侧顺时针),注意上面顶点y为正值,是IdleDown

image-20210703103354833

将混合模式改成离散模式(虚线)

image-20210703104848793

这样就已经实现了控制切换4个动画,不用写繁琐的代码了.拖动鼠标就可以查看效果

用同样的方式,制作run的混合动画

制作完毕后进行连线操作,无需设置转换方式保持Immediate即可

image-20210703171905942

修改脚本代码,这里我修改了几次,优化了一下,保证在滑动的过程中也能顺利播放动画

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
onready var playerAnimation = $AnimationPlayer
onready var playerAnimationTree = $AnimationTree
onready var playerAnimationState = playerAnimationTree.get("parameters/playback")

#func _ready():
#    playerAnimation = $AnimationPlayer

func _physics_process(delta):
    var input_vector = Vector2.ZERO
    input_vector.x = Input.get_action_strength("ui_right") - Input.get_action_strength("ui_left")
    input_vector.y = Input.get_action_strength("ui_down") - Input.get_action_strength("ui_up")
    # 控制器在轴移动的时候距离进行控制在一个圆形范围内
    input_vector = input_vector.normalized()
    velocity.x = move_towards_axis(input_vector.x, velocity.x, delta)
    velocity.y = move_towards_axis(input_vector.y, velocity.y, delta)
    # 播放动画
    # 将移动速度来作为播放动画的参数限制到0-1
    var animationValue = velocity.normalized()
    if animationValue != Vector2.ZERO:
        playerAnimationTree.set('parameters/Idle/blend_position', animationValue)
        playerAnimationTree.set('parameters/Run/blend_position', animationValue)
        playerAnimationState.travel("Run")
    else:
#        print(input_vector)
#        print(velocity)
        playerAnimationState.travel("Idle")
#    anim_tree.set("parameters/eye_blend/blend_amount", 1.0)
    # Simpler alternative form:
#    anim_tree["parameters/eye_blend/blend_amount"] = 1.0
    
    # 如果发生碰撞velocity会被改变,所以这里要重新赋值
    velocity = move_and_slide(velocity)
TAG: godot game
文章作者 : Cocding