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自學不成才
相關文章
相關標籤/搜索