一、前言
這一次,讓我們來做一些輕松有趣的東西,嘿嘿。 ?
在上一篇 Godot3游戲引擎入門之十四:剛體RidigBody2D節(jié)點的使用以及簡單的FSM狀態(tài)機介紹的文章中,我們主要討論了剛體節(jié)點 RigidBody2D 的一些常用屬性以及在游戲中的簡單使用,利用剛體節(jié)點開發(fā)了一個簡單的太空飛船射擊小游戲,這一章我們繼續(xù)探討剛體節(jié)點,研究一下剛體節(jié)點的其他幾個重要屬性,并在場景中做一些簡單應用。
除此之外,我還會穿插著介紹一下 Godot 引擎自帶的 AStar 最短路徑尋路 API 的簡單使用。
主要內容: RigidBody2D 剛體節(jié)點的幾個有趣的應用場景
閱讀時間: 10 分鐘
永久鏈接: http://liuqingwen.me/blog/2019/07/31/introduction-of-godot-3-part-15-several-usage-examples-of-rigidbody2d-node-in-games/
系列主頁: http://liuqingwen.me/blog/introduction-of-godot-series/
二、正文
廢話不多說,由于自己知識和經(jīng)驗的局限性,暫時我能想到的 RigidBody2D 的應用場景主要有這幾個:
剛體節(jié)點作為普通的游戲物品或者元素
剛體節(jié)點響應鼠標事件進行拖拽
利用剛體節(jié)點實現(xiàn)爆破特效
隨機生成地圖的應用
注:為了縮短文章篇幅,涉及到的代碼只提供核心部分,其他部分代碼將省略,有興趣的朋友可以直接到我的 Github 倉庫下載項目的全部源碼查看。
- 普通元素
在上一篇文章中,我們使用剛體節(jié)點制作了太空飛船和太空巖石,由于是在太空,它們都不會受到重力的影響。實際應用場景中,剛體默認會受到重力的作用,在重力影響下剛體會發(fā)生一些有趣的碰撞反饋,我們可以充分利用 RigidBody2D 剛體節(jié)點的物理特性,無需手動編寫代碼即可實現(xiàn)一些簡單的特效。
在這個場景中,木箱子和子彈球都是剛體模型,與我們之前游戲中使用 Area2D 作為根節(jié)點的“子彈”場景不同,使用 RigidBody2D 作為根節(jié)點,“子彈”可以直接和游戲世界中的其他物體產(chǎn)生碰撞互動。另外,游戲場景中玩家根節(jié)點為 KinematicBody2D 節(jié)點,能與剛體產(chǎn)生直接互動。從上圖中可以看出來,勾選和不勾選 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
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
影響玩家與剛體碰撞反饋核心方法是 KinematicBody2D 的方法 move_and_slide() ,這個方法在 Godot 3.1 版本中新增加了一個參數(shù),即最后一個參數(shù) infinite_inertia ,表示玩家是否為無限慣性。如果玩家具有無限慣性屬性,那么玩家移動時可以推動剛體,甚至擠壓物體,但是不會檢測與剛體的碰撞;如果玩家非無限慣性,那么剛體就像靜態(tài)碰撞體一樣會阻止玩家的移動。參數(shù)默認值為 true 表示無限慣性。其他的都比較簡單了,之前的文章也有討論。
- 鼠標拖拽
另一個有意思的應用場景是:我們可以使用鼠標來拖拽剛體進行移動,同時與其他剛體進行交互,最后使用鼠標將其“拋”出去。
實現(xiàn)這個效果不難,這里我們需要使用到剛體的另一個重要的屬性: Mode 屬性,即剛體的模式。在剛體屬性面板中,我們會發(fā)現(xiàn)該屬性有 4 種取值設置:
Rigid 即普通剛體模式,為默認值
Static 靜態(tài)模式,剛體表現(xiàn)和靜態(tài)碰撞體一樣
Kinematic 圖形學模式,和 KinematicBody2D 一樣
Character 人物模式,和普通剛體一樣,但是不會發(fā)生旋轉
利用這一點,我們可以找到實現(xiàn)剛體拖拽的思路:拖拽開始時刻設置剛體的模式為 MODE_STATIC 靜態(tài)模式,同時控制剛體的全局位置跟隨鼠標移動,拖拽結束即松開鼠標后,復原剛體的模式為 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 # 拾起盒子,更改為靜態(tài)模式
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) # 給盒子一個拋力
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
核心部分為 pickup() 和 drop() 這兩個方法,實現(xiàn)起來非常簡單,這里需要提醒的是,對于 RigidBody2D 剛體節(jié)點,如果需要響應鼠標事件,即 _input_event() 方法的正常調用,我們必須勾選設置剛體節(jié)點的 Pickable 屬性:
另外,在代碼中有一個值得注意的地方是,松開鼠標后,復原剛體模式為普通模式的同時不能讓其進入默認的睡眠狀態(tài)。阻止剛體睡眠狀態(tài)有兩種方法:
sleeping = false 即設置睡眠屬性
apply_central_impulse(Vector2.ZERO) 給剛體添加一個沖量,大小為 0 也可以
鼠標松開后,我們給物體一個拋力使其運動,所以我們選擇第二種方式即可。
- 爆破特效
“物品爆破”特效在游戲中很常見,可以直接使用動畫實現(xiàn),這里我講的是通過代碼來實現(xiàn)物體的爆破特效。我使用了 Github 上一個開源庫,非常容易地實現(xiàn)了爆破效果,開源庫鏈接地址: Godot-3-2D-Destructible-Objects 。如何使用這個開源庫在其主頁上有詳細的說明,實際使用過程中,我遇到了的一個問題,如下圖所示的場景結構圖:特效代碼不能直接放在需要爆破的子場景中,而應該放在子場景實例化后的節(jié)點上!
另外,源代碼中自帶的控制爆炸的方式是鼠標左鍵點擊事件,這里我稍微修改了一下源碼,讓效果只有在爆炸體與玩家或者子彈碰撞后才會觸發(fā),部分代碼如下:
引起爆炸的物體分組名集合,這里為玩家和子彈
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):
Area2D.queue_free()
return
1
2
3
4
5
6
7
8
9
大家可以自己嘗試,效果圖如下:
- 隨機地圖
在游戲中隨機生成地圖是一個非?!熬薮蟆?、非?!吧钊搿钡脑掝},不過本篇中我要介紹的隨機地圖生成只是涉及到其中的一點點皮毛,對這個話題感興趣的朋友可以到網(wǎng)上找找相關的資料。怎么生成一個隨機的地圖呢?我的思路大概是這樣的:
地圖由一個一個的小房間構成
房間之間沒有重疊,就像剛體不能互相交叉滲入一樣
房間個數(shù)、大小、位置都隨機
房間之間有路徑可達,整個地圖必須有一條完整的路徑
如何實現(xiàn)這個特別的“房間”呢?其實很簡單,我們可以使用 RigidBody2D 節(jié)點作為房間場景的根節(jié)點,充分利用其物理特性,這里最重要的一點就是設置剛體節(jié)點的 Mode 模式屬性為 Character 人物模式,以保證其不會發(fā)生旋轉:
同時,不需要考慮重力因素,設置重力影響系數(shù)設為 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)
1
2
3
4
5
6
7
8
9
接下來我們主要分三步實現(xiàn)隨機地圖的輪廓。第一步,我們在主場景中生成一定數(shù)量的大小隨機的房間,利用“人物”剛體模式的特性,房間添加到場景后會自動彼此分開;第二步,我們隨機地刪除一些房間,讓地圖顯得更加隨機;第三步,使用 AStar 尋路算法將我們產(chǎn)生的房間之間的最短路勁找出來。最后一步,肯定是替換“房間”為真正的“地圖”,這一步我就沒有介紹了,大家完全可以動手實現(xiàn)一個,或者參考我后面給出的相關資料。好了,我們看下效果:
主要的代碼如下:
export var roomScene : PackedScene = null # 房間子場景
export var roomCount : int = 25 # 房間總數(shù)量
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 # 剔除部分房間,系數(shù)
onready var _roomContainer := 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 # 相機偏移
隨機地圖生成方法,可以拆分為多個函數(shù),這里分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')
# 隨機刪除一部分房間,把房間的位置全部添加到數(shù)組,注意時 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.') # 第二步完成
# 創(chuàng)建新的AStar算法,添加第一個點
_astarPath = AStar.new()
_astarPath.add_point(_astarPath.get_available_point_id(), allPoints.pop_front())
# 循環(huán)所有【未添加的點】,循環(huán)所有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
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
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
代碼雖然有點長,不過并不難,相信大家很容易就能看懂,你完全可以把 generateRooms() 方法拆分為多個子方法來實現(xiàn),這里關于 AStar 的用法我已經(jīng)在注釋中作了簡要說明,形象一點,可以參考下圖:
另外,隨機生成房間的時候,你可以設置一下房間的坐標位置,比如放置在同一條水平線上等。這里我給大家看下最終的實現(xiàn)效果:
相關內容可以參考如下鏈接:
AStar API
http://kidscancode.org/blog/tags/procgen/
Procedural Generation in Godot - Part 6: Dungeons
三、總結
簡單的介紹了 RigidBody2D 節(jié)點的幾個應用場景,不知道大家感覺怎樣?有沒有更好玩的點子?期待大家的留言,哈哈。
本篇的 Demo 以及相關代碼已經(jīng)上傳到 Github ,地址: https://github.com/spkingr/Godot-Demos , 后續(xù)繼續(xù)更新,原創(chuàng)不易,希望大家喜歡! ?