LoginSignup
8
1

godot-goでゲーム開発

Last updated at Posted at 2023-12-17

本記事は ZOZO Advent Calendar 2023 カレンダー Vol.8の18日目の記事です。

はじめに

こんにちは、@prpr_manです。ZOZOには11月に入社したばかりで、現在はGo言語でZOZOTOWNのマイクロサービスの開発を行っています。

この記事は前日の@shogoLazyさんのGo言語で作ったゲームをWebで公開してみたに続いて、Go言語でゲーム開発をする事を目指します。こちらの記事ではEbitengineを使用していましたが、本記事ではGodotを利用してGo言語でゲーム開発を行ってみます。

Godotとは

Godotは、オープンソースかつクロスプラットフォームなゲームエンジンの一つです。ゲームエンジンというとUnityやUnreal Engineが有名ですが、Godotも近年注目を集めており、Godot製のゲームも多く登場しています。特にUnityが「ゲームインストール数をベースとした価格体系を導入する」と発表した1ときには、Godotに乗り換えると発表した開発チームもありました。

GodotでGo言語を使うには

Godotでサポートされている言語はC#とGDScriptと呼ばれるPythonライクなスクリプト言語です。Go言語はサポートされていません。
しかし、GodotにはUnityのネイティブプラグインのような、外部のネイティブコード(共有ライブラリ)を実行できるGDExtensionという機能があります。このライブラリからGDExtension APIを実行することでGodotと連携することができます。

今回はGDExtension APIのGoバインディングであるgodot-goを利用します。

実際に作ってみる

今回は題材としてPongを作ってみることにします。よりシンプルにするために、以下の3つのオブジェクトのみ作成し、勝敗判定やスコア計上などは行いません。
また開発環境はmacOSを想定して記載します。

  • Paddle
    • キー入力で上下に移動するオブジェクト。ボールを跳ね返す。
  • Ball
    • 画面上を移動するオブジェクト。PaddleやWallにあたると反射する。
  • Wall
    • 上下に設置される固定のオブジェクト。ボールを跳ね返す。

pong.gif

1. 環境構築

Godot

公式ページからGodotをダウンロードします。godot-goがバージョン4.2以降に対応しているため、4.2以降のバージョンをダウンロードしてください。

go言語

godot-goを利用するにはGo言語のバージョン1.21以降が必要です。

# インストール
$ brew install go

# インストールされたバージョンの確認
$ go version
go version go1.21.5 darwin/amd64

Godotの実行ファイルのシンボリックリンク作成

Godotをコマンドから利用したいため、シンボリックリンクを作成します。

$ ln -s /Applications/Godot.app/Contents/MacOS/Godot /usr/local/bin/godot

2. Godotプロジェクトの作成

適当なディレクトリを作成します。ここではpong-goとし、その配下にprojectディレクトリを作成します。

$ mkdir -p ./pong-go/project

Godotを起動して「新規」ボタンから新規プロジェクトを作成します。プロジェクトパスは先ほど作成した./pong-go/projectを指定します。
ss1.png

プロジェクトが作成されたら、今回は2Dゲームを作成するため画面上部のメニューから「2D」を選択します。
ss2.png

3.シーンの作成

Paddle、Ball、Wallのシーンを作成していきます。シーンとは画面を構成する部品(ノード)の集合です。Godotはこのシーンを組み合わせてオブジェクトや画面を作っていきます。Unityを利用したことがある人であれば「GodotのシーンとはUnityにおけるプレハブとシーンの両方の役割を持つもの」と考えると分かり易いかと思います。

Paddle、Ball、Wallシーンの作成

左のメニューから「その他のノード」を選択し、Area2Dを選択します。さらにColorRectCollisionShape2Dを追加し、以下のような階層構造にします。

Area2D
├─ColorRect
└─CollisionShape2D

CollisionShape2DのインスペクターにはShapeの項目がありますが、これはRectangleShape2Dとなるように設定します。その後、ColorRectCollisionShape2Dの形状をドラッグで調整します。最後にCmd+Sを押してpaddle.tscnとして保存します。
ss3.png

同様の手順でball.tscnwall.tscnを作成します。
ss4.png
ss5.png

Mainシーンの作成

上記で作成したシーンを配置しゲーム画面を作っていきます。左のメニューから「2Dシーン」を選択した後、ファイルシステムにあるpaddle.tscnball.tscnball.tscnをドラッグ&ドロップで配置していきます。画面上の青い線がWindowサイズなので、これに合わせて配置していくと良いでしょう。配置し終えたらmain.tcsnとして保存します。
ss6.png

3. コードを用意する

いよいよGo言語でコードを記述していきます。コードで制御したいシーンはPaddleとBallなのでこれらのコードを用意しましょう。

まずは./pong-goに移動してgo moduleの初期化を行います。続いて./pong-go/pkg/sceneディレクトリを作成します。

$ cd ./pong-go
$ go mod init pong-go
$ mkdir -p ./pkg/scene

作成した./pong-go/pkg/sceneディレクトリにPaddleとBallのコードを記述していきます。

paddle.go
package scene

import (
	"github.com/godot-go/godot-go/pkg/builtin"
	"github.com/godot-go/godot-go/pkg/constant"
	"github.com/godot-go/godot-go/pkg/core"
	"github.com/godot-go/godot-go/pkg/ffi"
	"github.com/godot-go/godot-go/pkg/gdclassimpl"
)

// 初期化用の関数
func RegisterClassPaddle() {
	core.ClassDBRegisterClass(&Paddle{}, []ffi.GDExtensionPropertyInfo{}, nil, func(t builtin.GDClass) {
		// V_Readyと_ready、V_Processと_processを紐付ける
		core.ClassDBBindMethodVirtual(t, "V_Ready", "_ready", nil, nil)
		core.ClassDBBindMethodVirtual(t, "V_Process", "_process", []string{"delta"}, nil)

        // インスペクター用の設定
		core.ClassDBAddPropertyGroup(t, "Settings", "group_")
		core.ClassDBBindMethod(t, "GetLeft", "get_left", nil, nil)
		core.ClassDBBindMethod(t, "SetLeft", "set_left", []string{"isLeft"}, nil)
		core.ClassDBAddProperty(t, ffi.GDEXTENSION_VARIANT_TYPE_BOOL, "group_main", "set_left", "get_left")
	})
}

const PaddleSpeed = 400

type Paddle struct {
	gdclassimpl.Area2DImpl
	input   builtin.Input
	isLeft  bool
	upKey   int
	downKey int
}

func (*Paddle) GetClassName() string {
	return "Paddle"
}

func (*Paddle) GetParentClassName() string {
	return "Area2D"
}

// _ready 一度だけ呼び出されるメソッド
func (p *Paddle) V_Ready() {
	p.input = gdclassimpl.GetInputSingleton()

    // インスペクターでLeftと設定されていたらWとSキーで上下させる
    // それ以外は上下キー
	if p.isLeft {
		p.upKey = constant.KEY_W
		p.downKey = constant.KEY_S
	} else {
		p.upKey = constant.KEY_UP
		p.downKey = constant.KEY_DOWN
	}
}

// _process フレーム毎に呼び出される更新用メソッド
func (p *Paddle) V_Process(delta float32) {
	velocity := builtin.NewVector2WithFloat32Float32(0, 0)
	if p.input.IsKeyPressed(constant.Key(p.downKey)) {
		velocity = velocity.Add_Vector2(builtin.NewVector2WithFloat32Float32(0, 1))
	} else if p.input.IsKeyPressed(constant.Key(p.upKey)) {
		velocity = velocity.Add_Vector2(builtin.NewVector2WithFloat32Float32(0, -1))
	}

	velocity = velocity.Normalized()
	velocity = velocity.Multiply_int(PaddleSpeed)

	position := p.GetPosition()
	position = position.Add_Vector2(velocity.Multiply_float(delta))

	p.SetPosition(position)
}

func (p *Paddle) SetLeft(isLeft bool) {
	p.isLeft = isLeft
}

func (p *Paddle) GetLeft() bool {
	return p.isLeft
}
ball.go
package scene

import (
	"github.com/godot-go/godot-go/pkg/builtin"
	"github.com/godot-go/godot-go/pkg/constant"
	"github.com/godot-go/godot-go/pkg/core"
	"github.com/godot-go/godot-go/pkg/ffi"
	"github.com/godot-go/godot-go/pkg/gdclassimpl"
)

// 初期化用の関数
func RegisterClassBall() {
	core.ClassDBRegisterClass(&Ball{}, []ffi.GDExtensionPropertyInfo{}, nil, func(t builtin.GDClass) {
		// // V_Readyと_ready、V_Processと_processを紐付ける
		core.ClassDBBindMethodVirtual(t, "V_Ready", "_ready", nil, nil)
		core.ClassDBBindMethodVirtual(t, "V_Process", "_process", []string{"delta"}, nil)

        // V_On_Area_Enteredと_on_Area_enterdを紐づける
		core.ClassDBBindMethodVirtual(t, "V_On_Area_Entered", "_on_Area_entered", []string{"area"}, nil)
	})
}

const (
	BallSpeed = 400
)

type Ball struct {
	gdclassimpl.Area2DImpl
	velocity builtin.Vector2
}

func (*Ball) GetClassName() string {
	return "Ball"
}

func (*Ball) GetParentClassName() string {
	return "Area2D"
}

func (b *Ball) V_Ready() {
	v := builtin.NewVector2WithFloat32Float32(1, 1)
	v = v.Normalized()
	b.velocity = v.Multiply_int(BallSpeed)

    // Area2D同士が接触したら_on_Area_enteredを呼び出す様に設定する
	b.connectSignal("area_entered", "_on_Area_entered")
}

func (b *Ball) V_Process(delta float32) {
	position := b.GetPosition()
	position = position.Add_Vector2(b.velocity.Multiply_float(delta))
	b.SetPosition(position)
}

func (b *Ball) V_On_Area_Entered(area builtin.Variant) {
	paddle := builtin.NewStringWithUtf8Chars("Paddle")
	defer paddle.Destroy()

	if area.ToObject().IsClass(paddle) {
		b.Bounce(builtin.NewVector2WithFloat32Float32(0, 1))
	} else {
		b.Bounce(builtin.NewVector2WithFloat32Float32(1, 0))
	}
}

func (b *Ball) Bounce(vec builtin.Vector2) {
	b.velocity = b.velocity.Reflect(vec)
}

func (b *Ball) connectSignal(signalName string, methodName string) {
	signal := builtin.NewStringNameWithUtf8Chars(signalName)
	defer signal.Destroy()
	method := builtin.NewStringNameWithUtf8Chars(methodName)
	defer method.Destroy()
	callable := builtin.NewCallableWithObjectStringName(b, method)
	defer callable.Destroy()
	b.Connect(signal, callable, constant.OBJECT_CONNECT_FLAGS_CONNECT_PERSIST)
}

最後にmain.goを記述します。exportで始まるコメントがないと動作しないため注意しましょう。

main.go
package main

import "C"
import (
	"pong-go/pkg/scene"
	"unsafe"

	"github.com/godot-go/godot-go/pkg/core"
	"github.com/godot-go/godot-go/pkg/ffi"
	"github.com/godot-go/godot-go/pkg/log"
)

//export PongGoInit
func PongGoInit(p_get_proc_address unsafe.Pointer, p_library unsafe.Pointer, r_initialization unsafe.Pointer) bool {

	initObj := core.NewInitObject(
		(ffi.GDExtensionInterfaceGetProcAddress)(p_get_proc_address),
		(ffi.GDExtensionClassLibraryPtr)(p_library),
		(*ffi.GDExtensionInitialization)(unsafe.Pointer(r_initialization)),
	)

	initObj.RegisterSceneInitializer(func() {
		scene.RegisterClassPaddle()
		scene.RegisterClassBall()
	})

	initObj.RegisterSceneTerminator(func() {
	})

	return initObj.Init()
}

func main() {
}

最後にmodule情報を更新します。

$ go mod tidy

4. ライブラリのビルド

以下の様なMakefileを作成してライブラリとしてビルドします。./pong-go/project/lib以下にライブラリとヘッダーファイルができればOKです。

Makefile
.DEFAULT_GOAL := build

GOOS?=$(shell go env GOOS)
GOARCH?=$(shell go env GOARCH)
GODOT?=$(shell which godot)

OUTPUT_PATH=project/lib
TEST_MAIN=main.go
TEST_BINARY_PATH=$(OUTPUT_PATH)/libgodotgo-pong-go-macos-$(GOARCH).framework

.PHONY: goenv build

goenv:
	go env

build: goenv
	CGO_ENABLED=1 \
	GOOS=$(GOOS) \
	GOARCH=$(GOARCH) \
	CGO_CFLAGS='-Og -g3 -g -DX86=1 -fPIC' \
	CGO_LDFLAGS='-Og -g3 -g' \
	go build -gcflags=all="-N -l" -tags tools -buildmode=c-shared -x -trimpath -o "$(TEST_BINARY_PATH)" $(TEST_MAIN)

editor:
	LOG_LEVEL=warn \
	GOTRACEBACK=1 \
	GODEBUG=sbrk=1,asyncpreemptoff=1,cgocheck=0,invalidptr=1,clobberfree=1,tracebackancestors=0 \
	$(GODOT) --verbose --debug  --path project/ --editor

$ make

5. Godotからライブラリを利用する

Godotからライブラリを利用するためには以下の様な.gdextensionファイルを./pong-go/project以下に作成する必要があります。
entry_symbolにはmain.goPongGoInitを指定し、[libraries]にはビルドしたライブラリを指定します。

[configuration]

entry_symbol = "PongGoInit"
compatibility_minimum = 4.1

[libraries]

macos.debug = "res://lib/libgodotgo-pong-go-macos-amd64.framework"
macos.release = "res://lib/libgodotgo-pong-go-macos-amd64.framework"

以上で必要なファイルはすべて作成しました。最終的には以下のようなディレクトリ構成になっているはずです。

pong-go
├─ pkg
│  └─ scene
│     ├─ ball.go
│     └─ paddle.go
├─ project
│  ├─ .godot
│  ├─ lib
│  │  ├─ libgodotgo-pong-go-macos-amd64.h
│  │  └─ libgodotgo-pong-go-macos-amd64.framework
│  ├─ ball.tscn
│  ├─ godotgo.gdextension
│  ├─ main.tscn
│  ├─ paddle.tscn
│  ├─ project.godot
│  └─ wall.tscn
├─ go.mod
├─ go.sum
├─ main.go
└─ Makefile

本来ならば以上の設定をすればGodotはPaddleやBallを認識し、Godot上からノードとしてPaddleやBallを追加できます。しかし今回使用するgodot-goではうまく認識してくれませんでした。
そこで今回はシーンファイルを直接変更してノードの型を書き換えます。 paddle.tcsnball.tcsnをエディタで開いて、該当箇所を以下の様に変更します。

paddle.tcsn
- [node name="Ball" type="Area2D"]
+ [node name="Ball" type="Paddle"]
ball.tcsn
- [node name="Paddle" type="Area2D"]
+ [node name="Paddle" type="Ball"]

この状態で、makeコマンドでeditorを起動し、上部のメニューの「▶︎」を押してみましょう。もしメインシーンを設定する画面が現れたらmain.tcsnを選択します。これでPongが動作するはずです。

$ make editor

ss7.png

左右でPaddleの操作キーを変えたい場合はmain.tscnを開いて、左側に配置したPaddleを選択します。そしてインスペクター上の「Setting」にあるチェックボタンを押すと、左のPaddleはWSキー、右のPaddleは上下キーで操作することができます。

まとめ

今回はgodot-goを用いてGo言語でゲームを作ってみました。godot-goはまだ開発途中であり、うまく動作しない箇所もあるため、Go言語だけで制作するのは難しい印象です。現状では一部の機能だけをGo言語で記載し、その他をGDScriptで作成するのが良さそうです。

参考

  1. 現在では撤回されています。(https://blog.unity.com/ja/news/open-letter-on-runtime-fee)

8
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
1