Godot3游戏引擎入门之十五:RigidBody2D刚体节点的几种应用场景及示例

Godot3游戏引擎入门之十五:RigidBody2D刚体节点的几种应用场景及示例

1、前言

这一次,让咱们来作一些轻松有趣的东西,嘿嘿。 :grin:html

在上一篇 Godot3游戏引擎入门之十四:刚体RidigBody2D节点的使用以及简单的FSM状态机介绍的文章中,咱们主要讨论了刚体节点 RigidBody2D 的一些经常使用属性以及在游戏中的简单使用,利用刚体节点开发了一个简单的太空飞船射击小游戏,这一章咱们继续探讨刚体节点,研究一下刚体节点的其余几个重要属性,并在场景中作一些简单应用。node

除此以外,我还会穿插着介绍一下 Godot 引擎自带的 AStar 最短路径寻路 API 的简单使用。python

主要内容: RigidBody2D 刚体节点的几个有趣的应用场景
阅读时间: 10 分钟
永久连接: liuqingwen.me/blog/2019/0…
系列主页: liuqingwen.me/blog/introd…git

2、正文

废话很少说,因为本身知识和经验的局限性,暂时我能想到的 RigidBody2D 的应用场景主要有这几个:github

  1. 刚体节点做为普通的游戏物品或者元素
  2. 刚体节点响应鼠标事件进行拖拽
  3. 利用刚体节点实现bao破特效
  4. 随机生成地图的应用

注:为了缩短文章篇幅,涉及到的代码只提供核心部分,其余部分代码将省略,有兴趣的朋友能够直接到个人 Github 仓库下载项目的所有源码查看。算法

1. 普通元素

上一篇文章中,咱们使用刚体节点制做了太空飞船和太空岩石,因为是在太空,它们都不会受到重力的影响。实际应用场景中,刚体默认会受到重力的做用,在重力影响下刚体会发生一些有趣的碰撞反馈,咱们能够充分利用 RigidBody2D 刚体节点的物理特性,无需手动编写代码便可实现一些简单的特效。数组

result_1.gif

在这个场景中,木箱子和子弹球都是刚体模型,与咱们以前游戏中使用 Area2D 做为根节点的“子弹”场景不一样,使用 RigidBody2D 做为根节点,“子弹”能够直接和游戏世界中的其余物体产生碰撞互动。另外,游戏场景中玩家根节点为 KinematicBody2D 节点,能与刚体产生直接互动。从上图中能够看出来,勾选和不勾选 player infinite inertia 选项,玩家和其余刚体的碰撞效果彻底不同,咱们先看下玩家 Player 场景的主要代码:微信

var _velocity := Vector2.ZERO
var _isInfInertia := true

func _physics_process(delta):
    var hDir := int(Input.is_action_pressed('ui_right')) - int(Input.is_action_pressed('ui_left'))
    var vDir := int(Input.is_action_pressed('ui_down')) - int(Input.is_action_pressed('ui_up'))
    var velocity := Vector2(hDir, vDir if isTopDown else 0).normalized() * moveSpeed
    if !isTopDown:
        velocity.y = _velocity.y + gravity * delta
    _velocity = self.move_and_slide(velocity, FLOOR_NORMAL, true, 4, PI / 2, _isInfInertia)

    # 省略代码……

func _shoot() -> void:
    if ! bulletScene || ! _canShoot:
        return
    _canShoot = false
    _timer.start()
    var ball := bulletScene.instance() as RigidBody2D
    ball.position = _bulletPosition.global_position
    ball.apply_central_impulse(bulletForce * _bulletPosition.transform.x)
    self.get_parent().add_child(ball)

# 设置玩家是否为无限惯性力
func setInfiniteInertia(value : bool) -> void:
    _isInfInertia = value
复制代码

影响玩家与刚体碰撞反馈核心方法是 KinematicBody2D 的方法 move_and_slide() ,这个方法在 Godot 3.1 版本中新增长了一个参数,即最后一个参数 infinite_inertia ,表示玩家是否为无限惯性。若是玩家具备无限惯性属性,那么玩家移动时能够推进刚体,甚至挤压物体,可是不会检测与刚体的碰撞;若是玩家非无限惯性,那么刚体就像静态碰撞体同样会阻止玩家的移动。参数默认值为 true 表示无限惯性。其余的都比较简单了,以前的文章也有讨论。app

2. 鼠标拖拽

另外一个有意思的应用场景是:咱们可使用鼠标来拖拽刚体进行移动,同时与其余刚体进行交互,最后使用鼠标将其“抛”出去。dom

result_2.gif

实现这个效果不难,这里咱们须要使用到刚体的另外一个重要的属性: Mode 属性,即刚体的模式。在刚体属性面板中,咱们会发现该属性有 4 种取值设置:

  • Rigid 即普通刚体模式,为默认值
  • Static 静态模式,刚体表现和静态碰撞体同样
  • Kinematic 图形学模式,和 KinematicBody2D 同样
  • Character 人物模式,和普通刚体同样,可是不会发生旋转

利用这一点,咱们能够找到实现刚体拖拽的思路:拖拽开始时刻设置刚体的模式为 MODE_STATIC 静态模式,同时控制刚体的全局位置跟随鼠标移动,拖拽结束即松开鼠标后,复原刚体的模式为 MODE_RIGID 普通模式,接着能够给刚体一个临时冲量使其运动。

export var mouseSensitivity := 0.25
export var deadPosition := 800.0

var _isPicked := false  # 判断当前刚体是否被鼠标拖拽

func _input_event(viewport, event, shape_idx):
    # 右键按下时拖拽箱子
    var e : InputEventMouseButton = event as InputEventMouseButton
    if e && e.button_index == BUTTON_RIGHT && e.pressed:
        pickup()

func _unhandled_input(event):
    # 右键松开时抛掉箱子
    var e : InputEventMouseButton = event as InputEventMouseButton
    if e && e.button_index == BUTTON_RIGHT && ! e.pressed:
        # 传入鼠标的移动速度
        var v := Input.get_last_mouse_speed() * mouseSensitivity
        drop(v)

func _physics_process(delta):
    # 更新拖拽盒子的位置,跟随鼠标移动
    if _isPicked:
        self.global_transform.origin = self.get_global_mouse_position()

    # 盒子掉出地图以外删除
    if self.position.y > deadPosition:
        self.queue_free()

func pickup() -> void:
    if _isPicked:
        return
    _isPicked = true
    self.mode = RigidBody2D.MODE_STATIC   # 拾起盒子,更改成静态模式

func drop(velocity: Vector2 = Vector2.ZERO) -> void:
    if ! _isPicked:
        return
    _isPicked = false
    self.mode = RigidBody2D.MODE_RIGID   # 抛掉盒子,更改成刚体模式
    # self.sleeping = false              # 防止刚体睡眠
    self.apply_central_impulse(velocity) # 给盒子一个抛力
复制代码

核心部分为 pickup()drop() 这两个方法,实现起来很是简单,这里须要提醒的是,对于 RigidBody2D 刚体节点,若是须要响应鼠标事件,即 _input_event() 方法的正常调用,咱们必须勾选设置刚体节点的 Pickable 属性

godot_15_pickable.jpg

另外,在代码中有一个值得注意的地方是,松开鼠标后,复原刚体模式为普通模式的同时不能让其进入默认的睡眠状态。阻止刚体睡眠状态有两种方法:

  • sleeping = false 即设置睡眠属性
  • apply_central_impulse(Vector2.ZERO) 给刚体添加一个冲量,大小为 0 也能够

鼠标松开后,咱们给物体一个抛力使其运动,因此咱们选择第二种方式便可。

3. bao破特效

“物品bao破”特效在游戏中很常见,能够直接使用动画实现,这里我讲的是经过代码来实现物体的bao破特效。我使用了 Github 上一个开源库,很是容易地实现了bao破效果,开源库连接地址: Godot-3-2D-Destructible-Objects 。如何使用这个开源库在其主页上有详细的说明,实际使用过程当中,我遇到了的一个问题,以下图所示的场景结构图:特效代码不能直接放在须要bao破的子场景中,而应该放在子场景实例化后的节点上!

godot_15_explosion_scene.jpg

另外,源代码中自带的控制bao zha的方式是鼠标左键点击事件,这里我稍微修改了一下源码,让效果只有在bao zha体与玩家或者子弹碰撞后才会触发,部分代码以下:

# 引发bao zha的物体分组名集合,这里为玩家和子弹
export(Array, String) var triggerGroups := ['player', 'bullet']

func _on_Area2D_area_or_body_entered(area_or_body):
    for group in triggerGroups:
        if area_or_body.is_in_group(group):
            $Explode.explode()
            $Area2D.queue_free()
            return
复制代码

你们能够本身尝试,效果图以下:

result_3.gif

4. 随机地图

在游戏中随机生成地图是一个很是“巨大”、很是“深刻”的话题,不过本篇中我要介绍的随机地图生成只是涉及到其中的一点点皮毛,对这个话题感兴趣的朋友能够到网上找找相关的资料。怎么生成一个随机的地图呢?个人思路大概是这样的:

  • 地图由一个一个的小房间构成
  • 房间之间没有重叠,就像刚体不能互相交叉渗入同样
  • 房间个数、大小、位置都随机
  • 房间之间有路径可达,整个地图必须有一条完整的路径

如何实现这个特别的“房间”呢?其实很简单,咱们可使用 RigidBody2D 节点做为房间场景的根节点,充分利用其物理特性,这里最重要的一点就是设置刚体节点的 Mode 模式属性为 Character 人物模式,以保证其不会发生旋转:

godot_15_room_property.jpg

同时,不须要考虑重力因素,设置重力影响系数设为 0 便可,房间场景 Room 的代码很是简单:

# 设置房间的位置和大小
func makeRoom(pos: Vector2, size: Vector2) -> void:
    self.position = pos
    _size = size

# 获取房间的位置尺寸,能够传入一个误差值
func getRect(tolerance : float = 0.0) -> Rect2:
    var s = _size - Vector2(tolerance, tolerance)
    return Rect2(self.position - s / 2, s)
复制代码

接下来咱们主要分三步实现随机地图的轮廓。第一步,咱们在主场景中生成必定数量的大小随机的房间,利用“人物”刚体模式的特性,房间添加到场景后会自动彼此分开;第二步,咱们随机地删除一些房间,让地图显得更加随机;第三步,使用 AStar 寻路算法将咱们产生的房间之间的最短路劲找出来。最后一步,确定是替换“房间”为真正的“地图”,这一步我就没有介绍了,你们彻底能够动手实现一个,或者参考我后面给出的相关资料。好了,咱们看下效果:

result_4.gif

主要的代码以下:

export var roomScene : PackedScene = null  # 房间子场景
export var roomCount : int = 25            # 房间总数量
export var tileSize : int = 32             # 地图瓦片单元尺寸
export var minSize : int = 4               # 房间最小尺寸,乘以瓦片尺寸
export var maxSize : int = 10              # 房间最大尺寸,乘以瓦片尺寸
export(float, 0.0, 1.0) var cullTolerance : float = 0.4  # 剔除部分房间,系数

onready var _roomContainer := $RoomContainer
onready var _camera := $Camera2D
onready var _windowSize : Vector2 = self.get_viewport_rect().size

var _isWorking := false                    # 是否正在进行生成中
var _astarPath : AStar = null              # AStar算法实例
var _zoom : Vector2 = Vector2.ONE          # 相机缩放
var _offset : Vector2 = Vector2.ZERO       # 相机偏移

# 随机地图生成方法,能够拆分为多个函数,这里分4步
func generateRooms() -> void:
    if ! roomScene || _isWorking:
        return

    # 标记,删除旧房间
    _isWorking = true
    _astarPath = null
    for room in _roomContainer.get_children():
        room.queue_free()

    # 随机生成新的房间,尺寸随机
    randomize()
    for i in range(roomCount):
        var room : Room = roomScene.instance()
        var width := randi() % (maxSize - minSize) + minSize
        var height := randi() % (maxSize - minSize) + minSize
        var size := Vector2(width, height) * tileSize
        room.makeRoom(Vector2.ZERO, size)
        _roomContainer.add_child(room)
    print('Step 1 is done.') # 第一步完成

    # 停留1秒,让生成的房间有足够时间分散开
    yield(self.get_tree().create_timer(1.0), 'timeout')

    # 随机删除一部分房间,把房间的位置所有添加到数组,注意时 Vector3 类型
    var allPoints : Array = []
    for room in _roomContainer.get_children():
        if randf() < cullTolerance:
            room.queue_free()
        else:
            room.mode = RigidBody2D.MODE_STATIC
            allPoints.append(Vector3(room.position.x, room.position.y, 0.0))
    print('Step 2 is done.') # 第二步完成

    # 建立新的AStar算法,添加第一个点
    _astarPath = AStar.new()
    _astarPath.add_point(_astarPath.get_available_point_id(), allPoints.pop_front())
    # 循环全部【未添加的点】,循环全部AStar中【已添加的点】
    # 找出【未添加点】与【已添加点】的距离中,【最短】的距离点,并添加到AStar中
    # 同时将该点从【未添加点集合】中删除
    while allPoints:
        var minDistance : float = INF
        var minDistancePosition : Vector3
        var minDistancePositionIndex : int
        var currentPointId :int = -1
        for point in _astarPath.get_points():
            for index in range(allPoints.size()):
                var pos = allPoints[index]
                var distance = _astarPath.get_point_position(point).distance_to(pos)
                if distance < minDistance:
                    minDistance = distance
                    minDistancePosition = pos
                    minDistancePositionIndex = index
                    currentPointId = point
        var id = _astarPath.get_available_point_id()
        _astarPath.add_point(id, minDistancePosition)
        _astarPath.connect_points(currentPointId, id)
        allPoints.remove(minDistancePositionIndex)
    print('Step 3 is done.') # 第三步完成

    # 等待一帧的时间,用于等待被删除的房间被完全移除
    yield(self.get_tree(), 'idle_frame')
    if _roomContainer.get_child_count() == 0:
        return

    # 找出全部房间最左上角和最右下角的两个坐标,肯定摄像机的缩放和位移
    var minPos := Vector2(_roomContainer.get_child(0).position.x, _roomContainer.get_child(0).position.y)
    var maxPos := minPos
    for room in _roomContainer.get_children():
        var rect := room.getRect() as Rect2
        if rect.position.x < minPos.x:
            minPos.x = rect.position.x
        if rect.end.x > maxPos.x:
            maxPos.x = rect.end.x
        if rect.position.y < minPos.y:
            minPos.y = rect.position.y
        if rect.end.y > maxPos.y:
            maxPos.y = rect.end.y
    _zoom = Vector2.ONE * ceil(max((maxPos.x - minPos.x) / _windowSize.x, (maxPos.y - minPos.y) / _windowSize.y))
    _offset = (maxPos + minPos) / 2
    print('Step 4 is done.') # 第四步完成

    _isWorking = false
复制代码

代码虽然有点长,不过并不难,相信你们很容易就能看懂,你彻底能够把 generateRooms() 方法拆分为多个子方法来实现,这里关于 AStar 的用法我已经在注释中做了简要说明,形象一点,能够参考下图:

Astar.gif

另外,随机生成房间的时候,你能够设置一下房间的坐标位置,好比放置在同一条水平线上等。这里我给你们看下最终的实现效果:

godot_dungeon_generation.gif

相关内容能够参考以下连接:

3、总结

简单的介绍了 RigidBody2D 节点的几个应用场景,不知道你们感受怎样?有没有更好玩的点子?期待你们的留言,哈哈。

本篇的 Demo 以及相关代码已经上传到 Github ,地址: github.com/spkingr/God… , 后续继续更新,原创不易,但愿你们喜欢! :smile:

个人博客地址: liuqingwen.me ,欢迎关注个人微信公众号:

IT自学不成才
相关文章
相关标签/搜索