這一次,讓咱們來作一些輕鬆有趣的東西,嘿嘿。 :grin:html
在上一篇 Godot3遊戲引擎入門之十四:剛體RidigBody2D節點的使用以及簡單的FSM狀態機介紹的文章中,咱們主要討論了剛體節點 RigidBody2D 的一些經常使用屬性以及在遊戲中的簡單使用,利用剛體節點開發了一個簡單的太空飛船射擊小遊戲,這一章咱們繼續探討剛體節點,研究一下剛體節點的其餘幾個重要屬性,並在場景中作一些簡單應用。node
除此以外,我還會穿插着介紹一下 Godot 引擎自帶的 AStar 最短路徑尋路 API 的簡單使用。python
主要內容: RigidBody2D 剛體節點的幾個有趣的應用場景
閱讀時間: 10 分鐘
永久連接: liuqingwen.me/blog/2019/0…
系列主頁: liuqingwen.me/blog/introd…git
廢話很少說,因爲本身知識和經驗的侷限性,暫時我能想到的 RigidBody2D 的應用場景主要有這幾個:github
注:爲了縮短文章篇幅,涉及到的代碼只提供核心部分,其餘部分代碼將省略,有興趣的朋友能夠直接到個人 Github 倉庫下載項目的所有源碼查看。算法
在上一篇文章中,咱們使用剛體節點製做了太空飛船和太空岩石,因爲是在太空,它們都不會受到重力的影響。實際應用場景中,剛體默認會受到重力的做用,在重力影響下剛體會發生一些有趣的碰撞反饋,咱們能夠充分利用 RigidBody2D 剛體節點的物理特性,無需手動編寫代碼便可實現一些簡單的特效。數組
在這個場景中,木箱子和子彈球都是剛體模型,與咱們以前遊戲中使用 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
另外一個有意思的應用場景是:咱們可使用鼠標來拖拽剛體進行移動,同時與其餘剛體進行交互,最後使用鼠標將其「拋」出去。dom
實現這個效果不難,這裏咱們須要使用到剛體的另外一個重要的屬性: Mode
屬性,即剛體的模式。在剛體屬性面板中,咱們會發現該屬性有 4 種取值設置:
利用這一點,咱們能夠找到實現剛體拖拽的思路:拖拽開始時刻設置剛體的模式爲 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
屬性:
另外,在代碼中有一個值得注意的地方是,鬆開鼠標後,復原剛體模式爲普通模式的同時不能讓其進入默認的睡眠狀態。阻止剛體睡眠狀態有兩種方法:
sleeping = false
即設置睡眠屬性apply_central_impulse(Vector2.ZERO)
給剛體添加一個衝量,大小爲 0 也能夠鼠標鬆開後,咱們給物體一個拋力使其運動,因此咱們選擇第二種方式便可。
「物品bao破」特效在遊戲中很常見,能夠直接使用動畫實現,這裏我講的是經過代碼來實現物體的bao破特效。我使用了 Github 上一個開源庫,很是容易地實現了bao破效果,開源庫連接地址: Godot-3-2D-Destructible-Objects 。如何使用這個開源庫在其主頁上有詳細的說明,實際使用過程當中,我遇到了的一個問題,以下圖所示的場景結構圖:特效代碼不能直接放在須要bao破的子場景中,而應該放在子場景實例化後的節點上!
另外,源代碼中自帶的控制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
複製代碼
你們能夠本身嘗試,效果圖以下:
在遊戲中隨機生成地圖是一個很是「巨大」、很是「深刻」的話題,不過本篇中我要介紹的隨機地圖生成只是涉及到其中的一點點皮毛,對這個話題感興趣的朋友能夠到網上找找相關的資料。怎麼生成一個隨機的地圖呢?個人思路大概是這樣的:
如何實現這個特別的「房間」呢?其實很簡單,咱們可使用 RigidBody2D 節點做爲房間場景的根節點,充分利用其物理特性,這裏最重要的一點就是設置剛體節點的 Mode
模式屬性爲 Character
人物模式,以保證其不會發生旋轉:
同時,不須要考慮重力因素,設置重力影響係數設爲 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 尋路算法將咱們產生的房間之間的最短路勁找出來。最後一步,確定是替換「房間」爲真正的「地圖」,這一步我就沒有介紹了,你們徹底能夠動手實現一個,或者參考我後面給出的相關資料。好了,咱們看下效果:
主要的代碼以下:
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 的用法我已經在註釋中做了簡要說明,形象一點,能夠參考下圖:
另外,隨機生成房間的時候,你能夠設置一下房間的座標位置,好比放置在同一條水平線上等。這裏我給你們看下最終的實現效果:
相關內容能夠參考以下連接:
簡單的介紹了 RigidBody2D 節點的幾個應用場景,不知道你們感受怎樣?有沒有更好玩的點子?期待你們的留言,哈哈。
本篇的 Demo 以及相關代碼已經上傳到 Github ,地址: github.com/spkingr/God… , 後續繼續更新,原創不易,但願你們喜歡! :smile:
個人博客地址: liuqingwen.me ,歡迎關注個人微信公衆號: