ぬーぶのメモ帳

フリーゲームエンジン「Godot」でプログラム経験のない素人の備忘録 最近は色々な動作を考えて試すのが好き。

【Godot】RTS風の移動方法を考える

マウスで移動をさせたい



リアルタイムストラテジー(Real-time Strategy)の移動方法にキャラクターを選択し目標を選択するとキャラクターが自動で移動するというものがあります。

自分はRTSで内政とキャラの操作量でパンクしますがそれでもキャラが自動で動いて作業をしている様子を見るのは好きです。

とりあえず簡単に再現できそうでRTS気分を味わえるのはマウスによる移動かなあと思い実験、よければお付き合いください。

キャラクターを用意

グラフィックはプロジェクト作成すると必ずついてくる Godot のアイコンを使います

プロトタイプのとき面倒で大体の人が使うGodot君

extends Sprite

export var speed := 50
var mouse_pos = Vector2()

var active := false
var can_move := false

func _input(event: InputEvent) -> void:
	# 左クリックをすると反応するように
	if event is InputEventMouseButton and event.button_index == BUTTON_LEFT:
		# キャラがアクティブの時だけマウス座標を取得する
		if active:
			mouse_pos = get_global_mouse_position()
			can_move = true
			active = false


func _process(delta: float) -> void:
	if can_move:
		position += (mouse_pos - position).normalized() * speed * delta
		# キャラクターと目標の距離が一定以下で停止する
		if position.distance_squared_to(mouse_pos) < 1.0:
			can_move = false
			return
	# もしかするとここはいらないかも
	else:
		pass


まずマウスをクリックした際にマウスの座標を取得できるようにしました

func _input(event: InputEvent) -> void:
	# 左クリックをすると反応するように
	if event is InputEventMouseButton and event.button_index == BUTTON_LEFT:
		# キャラがアクティブの時だけ座標を取得する
		if active:
			mouse_pos = get_global_mouse_position()
			can_move = true
			active = false

この時ほかの作業中に反応されても困るので active という bool型 変数 でフラグを作りキャラクターがアクティブ状態なら目的地の座標を取得。
更にcan_move という移動許可フラグをオン、そして最後クリックにまた反応しないようactive をオフにします。


キャラクター移動部分のコード

func _process(delta: float) -> void:
	if can_move:
		position += (mouse_pos - position).normalized() * speed * delta

		# 目標地点付近に到着すると停止する
		if position.distance_squared_to(mouse_pos) < 1.0:
			can_move = false
			return

上で登場した can_move の移動許可フラグがオンの場合 _process 内の移動命令が実行されます。

 (目標の座標 - 自分の座標) * 速度 は目標へ直線移動させる物に使え便利です。

 normalized() はざっくり言うとこれが無いと斜めの移動が少し速くなります。
 これ行うと縦横、斜めの移動速度が同じになります、STGなど一部ゲームでは意図的に斜め移動を早くしている物もあるようです。

移動命令の下のコマンド distance_squared_to(mouse_pos) は目標物と自身との距離を測っています。
上の if文 では マウスでクリックした位置 と 自身の距離が 一定の距離以下になった場合、移動許可のフラグがオフになり停止します。

※移動用のフラグをオンにした状態で実行


マウス選択の実装

キャラクターは用意できてもこのままではアクティブにならず動かせません。
今回はパソコンの操作でファイルをまとめて選択するような感じにしたいと思います。

全体コード

extends Node2D

var drag_start := Vector2()
var drag_end := Vector2()
var draw_drag := false


func _input(event: InputEvent) -> void:
	# マウスボタンの信号 かつ それが左ボタンだった場合
	if event is InputEventMouseButton and event.button_index == BUTTON_LEFT:
		# 上の条件からさらにボタンが押された場合
		if event.is_pressed():
			# マウスの位置を始点として取得
			drag_start = event.global_position
			drag_end = drag_start
			draw_drag = true
			
		# こちらはマウスボタンが離された場合になる
		else:
			# 取得された始点と終点から選択範囲を確定する関数
			get_unit(drag_start, drag_end)
			draw_drag = false

	# draw_drag がオンになっている状態でマウスが動くと選択範囲の終点を取得する
	if event is InputEventMouseMotion and draw_drag:
		drag_end = event.global_position
	
	# _draw の描画用コマンド
	update()
		

func _draw() -> void:
	# 選択範囲の四角を視覚的に表示
	if draw_drag:
		draw_rect(Rect2(drag_start, drag_end - drag_start),Color(1,1,1,0.5))


func get_unit(start, end)-> void:
	# 選択範囲になる四角を作成
	var rect = Rect2(start, end - start)
	
	
	# 選択対象が子にいることが前提
	for chara in get_children():
		# 選択範囲内に子が存在、 position が確認できるとき
		if rect.has_point(chara.position):
			# 子ノードの active フラグをオンにする
			chara.active = true


	# 選択範囲をクリア
	drag_start = Vector2.ZERO
	drag_end = Vector2.ZERO

まず選択範囲の始点と終点を取得します

func _input(event: InputEvent) -> void:
	# マウスボタン かつ それが左ボタンだった場合
	if event is InputEventMouseButton and event.button_index == BUTTON_LEFT:
		# 上の条件からさらにボタンが押された場合
		if event.is_pressed():
			# マウスの位置を始点として取得
			drag_start = event.global_position
			drag_end = drag_start
			draw_drag = true
		
		# こちらはマウスボタンが離された場合
		else:
			# 取得された始点と終点から選択範囲を確定する関数
			get_unit(drag_start, drag_end)
			draw_drag = false

	# draw_drag がオンになっている状態でマウスが動くと選択範囲の終点を取得する
	if event is InputEventMouseMotion and draw_drag:
		drag_end = event.global_position
	
	# _draw の描画用コマンド
	update()

次に処理される順番で説明します


■1 左クリックした場合 drag_start がクリックした位置を始点として記録します。
またdraw_drag というフラグもオンにします

	# マウスボタン かつ それが左ボタンだった場合
	if event is InputEventMouseButton and event.button_index == BUTTON_LEFT:
		# 上の条件からさらにボタンが押された場合
		if event.is_pressed():
			# マウスの位置を始点として取得
			drag_start = event.global_position
			drag_end = drag_start
			draw_drag = true

InputEventMouseButton は名前のとおり event がマウスボタンの時に反応するので マウスボタンを押した時だけでなく離した時にも反応 します。

is_pressed() はキーボード、マウスなどのデバイスでボタンが押された時 True を返します、ボタンを離した、ホールド状態を維持している場合 False を返します。
ホールドの反応が出るのは主にキーボードでしょうか、コントローラー等で試してないので気になる方は各自で確認して下さい。


■2 次に draw_drag のフラグがオンの場合マウスを動かす度マウスの位置を終点として記録する。
この段階で始点と終点を記録したので描画専用関数の _draw() で選択範囲を描画できます

	# draw_drag がオンになっている状態でマウスが動くと選択範囲の終点を取得する
	if event is InputEventMouseMotion and draw_drag:
		drag_end = event.global_position

	# _draw の描画用コマンド
	update()


func _draw() -> void:
	# 選択範囲の四角を視覚的に表示 フラグがオフになると消える
	if draw_drag:
		draw_rect(Rect2(drag_start, drag_end - drag_start),Color(1,1,1,0.5))

InputEventMouseMotion はマウスの動きを検知します、この時 event は InputEventMouseMotionの属性なので ■1 を無視し_Input関数 内をループするのでdraw_drag はオンのまま ■2 の条件を満たし終点を取得。
(■1 を無視するので drag_start で記録した座標も上書きされず残ります)

四角を描く準備ができた状態で 描画用のコマンド update() を通過して四角がリアルタイムに伸び縮みする姿が見れます。


■3 最後に左クリックを戻すと InputEventMouseButton がマウスボタンを離した事を検知し is_pressed() は False 判定になるため下の命令が実行されます。
範囲内のキャラクターをアクティブにする関数とdraw_drag がオフになり描画された四角が消えます。

		# こちらはマウスボタンが離された場合
		else:
			# 取得された始点と終点から選択範囲を確定する関数
			get_unit(drag_start, drag_end)
			draw_drag = false

キャラクターを選択する部分のコードです

func get_unit(start, end)-> void:
	# 選択範囲になる四角を作成
	var rect = Rect2(start, end - start)
	
	# 選択対象が子であることが前提
	for character in get_children():
		# 選択範囲内に子が存在、 position が確認できるとき
		if rect.has_point(character.position):
			# 子ノードの active フラグをオンにする
			character.active = true
	
	# 選択範囲をクリア
	drag_start = Vector2.ZERO
	drag_end = Vector2.ZERO

まずは選択範囲となる Rect2 を取得した始点と終点で作成
次にキャラクターを格納しているコンテナからノードを取得 for文 で繰り返し処理をする

※格納イメージ

 has_point(Vector2)Rect2 内に()で指定した物の座標 Vector2 が存在していたら True を返すコマンドです。

つまり選択範囲内にキャラクターが存在する対象の active フラグをオン、目標地点の座標を入手できる状態にします。



! 今回あらかじめコンテナ内にキャラクターを入れてそれらを取得する方法をとりました。
has_point()は四角の中に何かがあるかは判定できても、中にある物を取得する事は出来ないので予めノードを取得が必要なのでこのような形にしてみました

仮に get_tree().get_nodes_in_group("グループ名") のようにツリー全体を検索するより特定のノードに格納した方が効率がいいと考えました。

雑にノードを取得する方法は思ったより限られているようです。
(自身が扱いきれてない、もっと効率的な方法がある可能性の方が高いですが)



選択範囲も完成しキャラクターを何体か複製した状態で動作確認したものがこちらになります。

問題として今回の選択範囲は左上から右下の動作でしか選択を受け付けません。
右下から左上、右上から左下のような別のパターンに対応させるには改良が必要です。


まとめ
ここからキャラクターに指示など増やしていけば簡単なRTS風の物は作成できそうです。

ただ上記の選択範囲や直線移動しかできず障害物に引っかかるなど問題も色々あります。
障害物を避けるような動作を加える場合は更に高度な方法を要求されます。

しかし最初の目的であるキャラクターのアクティブ化、クリックでの目標地点の指定、目標へ移動と停止は成功したので概ね満足しています。
また InputEvent 関係はもっと調べれば面白いことができるのではないかと感じました。

以上となります、最後まで読んでくださりありがとうございました。