年末年始なのでGodotでゲームでも作ってみる 5日目

今日の目標

  • Nodeを移動させる
  • ゲームを作り始める

年末年始一週間あると思ってたら6日しかなかった。急いでゲーム作り始めないとまずい。

1フレーム毎にNodeを動かす

Godotで毎フレーム何かしらの処理をしたい場合はIdle processingとPhysics processingという二つの機構が用意されている。後者は基本的に物理演算を使うゲーム用で、今回は物理演算は使っていないので前者のIdle processingの機構を使ってNodeに何かしらの処理を加えてみる。Nodeにアタッチしたスクリプトに_process(delta)という関数を定義すると、1フレーム毎にこの関数が呼び出される。注意しなければいけないのは、フレームレート(FPS)は可変であるため、この中で処理をするときは基本的にdelta(前のフレームからの経過時間)をみて計算を行う必要がある。

func _process(delta):
	self.position = Vector2(self.position.x + delta * 100, self.position.y)

例えば上のようなコードを書くと、フレームが更新されるごとに、自身のNodeの座標(position)が右にdelta * 100ピクセル分移動していく(つまり1秒に100ピクセル)。

4日までに作っていたプロジェクトで、子Sceneの方に上記の_process(delta)を定義して実行すると、それぞれの子Sceneが少しずつ右に動いていく様子。

画面端まで行ったNodeをワープさせる

勉強がてら、もう少しこのプロジェクトをいじってみる。子SceneのNodeが画面右端に到達したら画面の左側にワープさせて、画面内をループするようにしてみたい。

func _process(delta):
	var children = get_tree().get_nodes_in_group("mygroup")
	for child in children:
		if child.position.x >= get_viewport_rect().size.x:
			child.position = Vector2(-100, child.position.y)

今度は親Sceneの方にも_process(delta)を定義した。get_tree().get_nodes_in_group(“mygroup”)で、グループに所属するNode(この場合は生成した子SceneのNode)のリストを取得できるので、それぞれのNodeに対して、位置のx座標(position.x)が画面の右端に到達していないかを調べる。画面のサイズはget_viewport_rect().sizeで取得することができる(get_viewport().sizeを使うと後で画面のストレッチを指定した時におかしくなるので注意)。もし画面の右端に到達していた場合はx座標を-100の位置に移動する。0を指定すると画面左端に急に出現したような感じになってしまうため、100ピクセル分左に移動して画面の外側から出現するようにしている。

スクリプティングまとめ

以上でなんとなくGodotにおけるスクリプトの書き方、Node / Sceneの扱い方は分かったので、これからもう少しゲームらしいものを実際に作っていこうと思う。冬休みはあと1.5日しかないけど。

新しいプロジェクトを作る

新しいプロジェクトを作る。どういうゲームを作るのかについては、今は秘密だが名前はHaetatakiにした。OpenGLはES 3.0を使用。

何を作るかは最後まで秘密にしておきたいが、とりあえずハエの画像が配置されているだけのHae.tscnというSceneを作る。これを敵キャラのSceneとして使いたい。

もう一つ別に空の2D Sceneを作り、GameStage.gdというスクリプトをアタッチする。

extends Node2D

const HaeScene = preload("res://Hae.tscn")

var rng = RandomNumberGenerator.new()

# Called when the node enters the scene tree for the first time.
func _ready():
	rng.randomize()
	
	for n in 10:
		var anInstance = HaeScene.instance()
		anInstance.name = "Hae" + str(n)
		anInstance.position = Vector2(rng.randf_range(0.0, get_viewport_rect().size.x), rng.randf_range(0.0, get_viewport_rect().size.y))
		anInstance.add_to_group("enemies")
		self.add_child(anInstance)

_ready()の中で10個ほどHae.tscnのインスタンスをランダムな位置に追加する。Godotでランダムな数値を使いたいときはRandomNumberGeneratorを使用できる。

まだどんなゲームになるのか予想がつかないが、とりあえずハエが10匹画面上のランダムな位置に表示されるようになった。

標的をランダムな方向に動かしてみる

Hae.tscnにもスクリプトをアタッチする。ファイル名はHae.gdとした。ランダムな方向に動かしたいので、orientationというプロパティを追加し、_process(delta)の中でそのorientationの方向に毎秒30ピクセルのスピードで動くようにコードを書く。

extends Node2D

var orientation = 0

func _process(delta):
	var speed = 30 * delta
	self.position = Vector2(self.position.x + speed * cos(orientation), self.position.y + speed * sin(orientation))

なお、Godotの2D座標系は下方向がY軸プラスになる座標系なので、数学でよくある上方向がプラスになる座標系と逆になっていることに注意したい。

extends Node2D
...
func _ready():
	rng.randomize()
	
	for n in 10:
		var anInstance = HaeScene.instance()
		anInstance.name = "Hae" + str(n)
		anInstance.orientation = rng.randf_range(0.0, PI * 2) // Added
		anInstance.position = Vector2(rng.randf_range(0.0, get_viewport_rect().size.x), rng.randf_range(0.0, get_viewport_rect().size.y))
		anInstance.add_to_group("enemies")
		self.add_child(anInstance)

親Sceneの方で、Haeのインスタンスを生成する際にorientationをランダムに設定するように変更。orientationの値はラジアン値で0.0〜6.28(PI * 2)の間でランダムに決定する。

実行すると、ハエがそれぞれランダムな方向に移動していく。

画面の端に行ったら反対の端にワープさせる処理をかく。

extends Node2D

const HaeScene = preload("res://Hae.tscn")

var rng = RandomNumberGenerator.new()

const STAGE_MARGIN = 100

# Called when the node enters the scene tree for the first time.
func _ready():
	...

func _process(delta):
	var stageEdgeLeft = -STAGE_MARGIN
	var stageEdgeRight = get_viewport_rect().size.x + STAGE_MARGIN
	var stageEdgeTop = -STAGE_MARGIN
	var stageEdgeBottom = get_viewport_rect().size.y + STAGE_MARGIN
	for enemy in get_tree().get_nodes_in_group("enemies"):
		if enemy.position.x < stageEdgeLeft:
			enemy.position = Vector2(stageEdgeRight, enemy.position.y)
		if enemy.position.x > stageEdgeRight:
			enemy.position = Vector2(stageEdgeLeft, enemy.position.y)
		if enemy.position.y < stageEdgeTop:
			enemy.position = Vector2(enemy.position.x, stageEdgeBottom)
		if enemy.position.y > stageEdgeBottom:
			enemy.position = Vector2(enemy.position.x, stageEdgeTop)
		

親Sceneの_process(delta)の中で各ハエのインスタンスを監視し、上下左右それぞれのステージの端に到達していたら反対の端にワープさせる。

タッチ判定を入れる

マウスで標的となるハエがクリックされたことを検知できるようにしてみよう。子SceneのHae.tscn自体に、クリックされたときのコールバックを実装しても良いのだが、今回は親Sceneの方にコールバックを実装する形をとった。理由としては、標的が重なっていたりした場合に複数の標的に同時にクリック判定をしたいことと、標的がいないところをクリックした時にも何かしらの処理を行いたいからだ。

extends Node2D

const HaeScene = preload("res://Hae.tscn")

var rng = RandomNumberGenerator.new()

const STAGE_MARGIN = 100
const ENEMY_RADIUS = 50

...
		
func _input(event):
	if event is InputEventMouseButton and event.button_index == BUTTON_LEFT and event.pressed:
		for enemy in get_tree().get_nodes_in_group("enemies"):
			var distance = event.position.distance_to(enemy.position)
			if distance <= ENEMY_RADIUS:
				enemy.position = Vector2(rng.randf_range(-STAGE_MARGIN, get_viewport_rect().size.x + STAGE_MARGIN), -STAGE_MARGIN)

マウスクリックはコールバック関数_input(event)内で取得できる。_input(event)はマウスクリック以外にもキーボードのキーが押された時やマウスのカーソルが押された時など様々な時に呼び出されるため、まずif文でマウスの左ボタンが押された時のみ処理が走るようにする。

if event is InputEventMouseButton and event.button_index == BUTTON_LEFT and event.pressed:

クリックされた場所は event.position でVector2型の座標として取得できる。Vector2には別のVector2との間の距離を計算するdistance_toメソッドが用意されているので、これを使ってステージ上にいる各標的(ハエ)との距離を調べる。距離がENEMY_RADIUSよりも小さかった場合に「あたり」と判定する。あたりと判定した場合は、とりあえずその標的をステージの上端のどこか見えない場所にランダムでワープさせている。

点数をカウントする

点数を表示させるためのLabelをGameStageに配置する。名前はScoreLabelとしておいた。

extends Node2D

const HaeScene = preload("res://Hae.tscn")

var rng = RandomNumberGenerator.new()

const STAGE_MARGIN = 100
const ENEMY_RADIUS = 50

var score = 0

...
		
func _input(event):
	if event is InputEventMouseButton and event.button_index == BUTTON_LEFT and event.pressed:
		for enemy in get_tree().get_nodes_in_group("enemies"):
			var distance = event.position.distance_to(enemy.position)
			if distance <= ENEMY_RADIUS:
				enemy.position = Vector2(rng.randf_range(-STAGE_MARGIN, get_viewport_rect().size.x + STAGE_MARGIN), -STAGE_MARGIN)
				score += 1
		get_node("ScoreLabel").text = "Score: " + str(score)

scoreという変数を用意し、標的がクリックされる毎に1を追加する。そして、マウスクリックがある度に最新のscoreの値をScoreLabelに表示する。

実際に動かして、点数が加算されていくことを確認。

まとめ

急に駆け足になってしまったが、一旦ゲームっぽい物の骨格を作ることができた。明日はこれをベースにもうちょっとゲームっぽくしていきたいと思う。

Pocket