BioErrorLog Tech Blog

試行錯誤の記録

エラー対処: オブジェクトの中の少なくとも1個がパスでないため、ブーリアン演算できません | Inkscape

オブジェクトの中の少なくとも1個がパスでないため、ブーリアン演算できません

というエラーが出て、Inkscapeのパスオブジェクト操作ができないときの対処法をメモします。

はじめに

この重なり合った二つの歯車状のパスオブジェクトを、"パス"タブから"差分"をクリックして差分抽出しようとした時に、以下のエラーが出て差分抽出が失敗しました。

オブジェクトの中の少なくとも1個がパスでないため、ブーリアン演算できません

※ エラーはInkscapeエディタの下の方に表示されます。

エラーメッセージにはオブジェクトの中の少なくとも1個がパスでないためとありますが、選択中の二つのオブジェクトはどちらもパスに変換済みのものです。

このエラーの対処法をメモします。

エラー対処法

パス操作したいオブジェクトの「グループ解除」します。

グループ化されているオブジェクトは、ブーリアン演算(差分、統合など)ができません。

オブジェクトのグループ化を解除しないと、ブーリアン演算ができない

特にこの歯車オブジェクトのように、自分ではグループ化していなくとも、デフォルトでグループ化されている場合があるので注意が必要です。

おわりに

以上、Inkscapeのブーリアン演算時のエラー対処法をメモしました。

どなたかの参考になれば幸いです。

余談: こうして作った歯車はこう使っています↓

[関連記事]

www.bioerrorlog.work

www.bioerrorlog.work

参考

Inkscape tutorial: 上級 | Inkscape

GodotからEbitengineに乗り換えた理由を整理する | Devlog #4

ゲーム開発記録その4。

GodotからEbitengineに乗り換えていたので、その理由と所感を整理します。

前回はこちら:

www.bioerrorlog.work

はじめに

ここしばらく、Ebitengineを触っていました。

Ebitengineは、Go製のシンプルな2Dゲームエンジンです。

過去のDevlogの通り、私はもともとGodot Engineを好んで使っていたのですが (一応UnityやUnreal Engine, pygameなども触っていたことはある)、今はEbitengineをメインに使っていこうかな、という気持ちになっています。

Ebitengineのどこが好きになれたのか、個人的な所感を残します。

GodotからEbitengineに乗り換えた理由

ツールに詳しくなる vs 自分で実装する

Godotでは、基本的にはあらかじめ用意された要素(Node等)をベースにゲームを実装していきます。

一方、Ebitengineには最小限のシンプルなAPIしかありません。 実現したいことは、基本自分でコードを書いて実装することになります。

どちらの思想が合うかは、作りたいものの特性や利用者のバックグラウンドによるでしょう。 私の場合は、Ebitengineを使った開発体験の方が好きでした。

Godotでの開発体験では、しばしばこのようなことが起こります:

  1. こんな実装をしたい
  2. それを実装するのに使える機能/Nodeはどんなのがあるのか調べる
  3. それを使って機能を実装する
  4. -> 上手く実装できない
  5. 機能/Nodeの仕様を詳しく調べる
  6. やりたかったことが微妙にサポートされていないことに気が付く
  7. -> 2.に戻る or 回避策を考える

巡り合わせが悪いと、「どう実装すれば良いか」よりも「この既存機能はどのような仕様なのか」を調べていじくりまわす時間と労力の方が長くなってしまうのです。 これが個人的にはストレスでした。 自分は何と戦ってるんだろう..?という気持ちになってくるんですね。

私はゲーム開発を本業とする界隈人ではないので、ゲームエンジンに備わる既存機能やプラクティスに対する知識も大してありません。 作りたいゲームは、決して複雑ではない2Dゲームです。 既存機能を調べながら悩むよりも、自分で考えた方が早いことも少なくないことに気づきました。

自前で実装してると失敗も多くしますし、Godotだったら一瞬でできたのに、みたいなこともあります。 しかし、ゲームを「コントロールできてる感」は格別です。

エンジンをフル活用できる程の既存知識もなく、複雑な機能を必要としてなかった私にとっては、シンプルな仕組みさえあれば良かったのだな、と感じました。 さながら、「顧客が本当に必要だったもの」の風刺画を思い出します。

「顧客が本当に必要だったもの」 | 画像はこちらから引用

AIの恩恵を受けやすい

ChatGPTをはじめとするLLMの登場により、私の普段の開発体験は大きく変わりました。 ChatGPTとのペアプロによってコードを書いています。

そうなった時に、これらAIとの相性 - AIペアプロがやりやすいか、AIの恩恵を受けやすいか、というのも一つの観点として重要です。

その意味で、EbitengineはAIとの相性が良いです。

EbitengineにはGUIがなく、コードとコマンドで開発が完結します。 独自言語/独自仕様に依る部分も少なく、世間的にも広い領域で使われているGo言語で書けます。 プロジェクトの全体像をAIに教えるのにはパッケージのツリー構成や単にtree結果を渡せば済みますし、実装も完全にコードの世界で完結するので、テキストでのやり取りを基本とするAIペアプロに向いています。 もちろん彼らはGo言語にも詳しいです。

これがGodotだと少し事情が変わってきます。 GUI要素が多くあるので、例えば「XXタブを開いたところにあるこの設定値を変更してください」みたいなやり取りが発生することがあるのですが、これがなかなか難しい。 GUIエディタ内を探し回った挙句、いやどこにも見つからないんだけど...みたいなことがよく起きるんですね。

GodotのGUIエディタの様子

また、GodotではGDScriptという独自言語を使いますが、彼らAIはこれにそこまで詳しくありません。 純粋に学習量が一般的な汎用言語に比較して少ないということもあるかも知れませんが、シンプルに最近の言語仕様の変更に追随できていないことの影響が大きいです。 最近の言語仕様の変更に追随できてない、というのは(AIの学習タイミングの関係上)もちろんGoにも同じことが言えるのですが、バージョンで言語仕様が結構変わるGDScriptは、比較してそのつらみをよく感じます。

ということで、Godotと比べてEbitengineがAIペアプロとの相性が良い、というのは嬉しかったポイントです。

小さい歩幅で前進する

個人開発における最悪の状況というのは、質の悪いものを作ってしまうことでも開発に時間がかかることでもなく、「開発をやめてしまうこと」や「飽きてしまうこと」だと思っています。

そのような状況を回避するためには、小さい歩幅でも開発し続けることが効果的です。

有名どころのゲームエンジンを使って開発を始めたはいいものの、よく分からなくて詰まってしまった、とか、イライラして開発を投げてしまった、という方は、シンプルな思想のEbitengineを使ってみてはいかがでしょうか。

上に挙げたような理由から、私には「リズムよく開発を続ける」ことにEbitengineが効果的でした。

おわりに

GodotからEbitengineに乗り換えた理由を現時点の所感から整理しました。

最近のUnityの動向に伴ってUnity代替としてのGodot Engineの注目度が高まる中で逆行ではあるかもしれませんが、現時点の考えとして書き残しておきます。

以上、どなたかの参考になれば幸いです。

[関連記事]

www.bioerrorlog.work

www.bioerrorlog.work

www.bioerrorlog.work

雰囲気で味わうTGS2023 初体験レポ | Tokyo Game Show 2023

Tokyo Game Showに初めて行ってきたので、体験レポを残します。

Tokyo Game Show 2023

はじめに

昨日(2023/09/23 土曜日)、Tokyo Game Show 2023に行ってきました。

もともと私にとってTokyo Game Showは、

  • 名前は知ってる。デカいゲーム展示イベントだよね?
  • "Tokyo"とあるのだから東京で開催してるんだろう
  • 一般人も入れるの?それとも対業界人向けのイベント?
  • もし一般人も入れるのであれば、せっかくだから行ってみたい

くらいの理解度でした。

私と同じように、前情報を一切知らないけどなんとなくTokyo Game Showに行ってみたい!という方に向けて、体験レポを残します。

TGS2023 初体験記

チケットを買う

まずは何より、チケットを買う必要があります。

公式ホームページを見てみると、日程は全4日間であり、前半2日はビジネスデイ、後半2日は一般公開日になってるようです。

チケット情報 | 公式ホームページより

チケットは全て日付指定で、当日券の販売はありません。 事前にチケットをネットで購入します。

一般チケットは税込2,300円でした。

ちなみにグッズや優先入場の特典がつくプレミアムチケットは4,000円で販売されてますが、私が買ったとき(イベント一週間前)にはもう売り切れでした。

チケット購入経路は色々なサービスが選択肢として提供されています。 基本的にはネットで購入して、事前にコンビニなどで発券する仕組みのようです。

チケット購入方法が色々提示されている | TGS公式ページより

私はたまたまアカウントを持っていたローソンチケットで購入し、ローソンの発券機でチケットを発行しました。

当日: 会場に向かう

いよいよ当日、イベント会場の幕張メッセに向かいます。 (余談ですが幕張メッセは東京ではなく千葉にあるらしい)

幕張メッセは行ったことがなかったのですが、最寄りの海浜幕張駅で降りて人の流れについて行くと、無事会場につきました。 会場に着いたのがだいたい13:00くらいです。

海浜幕張駅から会場の幕張メッセに至る道には、人の流れができているので迷うことはなかった

会場は大きく二つに分かれていて、インディーゲームやVR関連の出展が並ぶインディー会場(Hall 9-11)と、メインの企業ブースが並ぶメイン会場(Hall1-8)があります。

会場はメイン会場とインディー会場の二つに大きく分かれている

まずは混雑してなさそうなインディー会場に向かいます。

会場に着くと、入場待ちの行列が並んでました。

入場待ちの行列。この先もあと倍くらい行列が伸びている

結局、40分ほど待って会場入り口へ。

会場入り口では、チケットの確認と手荷物検査が行われます。

入場者には紙のリストバンドが配られて、以降はこれをつけてれば再入場が可能です。

入場者用のリストバンド

インディー会場

インディー会場には、開発チームやパブリッシャー単位でブースが並んでいます。

前情報では、会場は空調が効いてなくて暑いという噂もありましたが、この日はしっかり空調も効いてて快適でした。

インディー会場の様子

各ブースにはスタッフの方や開発者の方が控えていて、試遊用の実機がその場で遊べるようになってました。 構造としては学会のポスター発表に近い感じです。

各出展者のブースが並んでる様子

私は結局試遊はしませんでしたが、見て回るだけでも色々なゲームがあって楽しめました。

ちなみにインディー会場の近くにはコスプレ会場もあって、こちらもなかなか盛り上がってました。

メイン会場

メイン会場の方は、インディー会場とはまた大きく雰囲気が違います。

各大企業がこぞって派手なイベントを爆音で開催していました。

ブースは大きなモニターで飾られ、でかいモニュメントが展示されてたり、企業専属のコスプレさん(コンパニオンさん?)がいたり、バンド演奏や演劇、トークイベントなどで盛りだくさんです。

KONAMIブース。バンド演奏やメタルギアスペースで盛り上がっている

スクエニブース。トークイベントに多くの人が押しかける

カプコンブース。ドラゴンの巨大モニュメントが展示されてる

SEGA / ATLUSブース。演劇イベントで盛り上がってた

会場の雰囲気としては、一つの建物の中にライブ会場がひしめいているような熱気がありました。

メイン会場の人気ブースの試遊は、整理券制だったり行列が並んでいたりと、なかなか難易度が高そうでした。 人気ブースの試遊をするなら、朝から来るなど工夫が必要そうです。

メイン会場の全体としては、ゲームの展示を見るというより、各企業が開催するイベントを楽しむのがメインの空気のようにも感じました。

感想

結局3-4時間ほどの滞在でしたが、ゲーム業界の熱気と市場規模を肌で感じる一日でした。 私にとってゲームをプレイするというのは、部屋にこもってモニタに向かう個人的な体験であるというイメージが強かったので、多くのゲームファンが物理的に集まって盛り上がっているイベントの光景は新鮮でした。 良いものですね。

参加者全体の人数構成としては、外国の方の割合が多かったのも印象的です。 おそらく観客側も出展者側も、3割ほどは海外からだったのではないでしょうか。 男女構成や年齢層も、文字通り老若男女どの層もいました。 ゲームがあらゆる属性の人々のエンタメであることを実感します。

おわりに

以上、TGS2023に初参加してみました、の記録でした。

よくわからないけど自分も行ってみたいなーという方の参考になれば幸いです。

[関連記事]

www.bioerrorlog.work

www.bioerrorlog.work

www.bioerrorlog.work

参考

TOKYO GAME SHOW 2023 - 東京ゲームショウ2023

Godot Engineでパラメータテストを書く | GUT

Godot EngineのGUTプラグインを使って、パラメータテスト/parameterized testを書く方法を整理します。

はじめに

以前、Godot Engineでテストを書く方法をまとめました: www.bioerrorlog.work

今回はそれに加えて、パラメータテストを書く方法をメモします。

Godot Engineでパラメータテストを書く

テスト関数の引数に、params=use_parameters(<List>)の形でListを渡せば、そのListの要素の数だけ繰り返しテストが実行されます。

例:

# テスト対象
class Foo:
  func add(p1, p2):
    return p1 + p2
# テストコード
extends GutTest

var foo_params = [[1, 2, 3], ['a', 'b', 'c']]

func test_foo(params=use_parameters(foo_params)):
  var foo = Foo.new()
  var result = foo.add(params[0], params[1])
  assert_eq(result, params[2])

Ref. Parameterized Tests · bitwes/Gut Wiki · GitHub

1回目のテストではparams[1, 2, 3]が、2回目のテストではparams['a', 'b', 'c']が割り当てられ、パラメータとして利用できます。

例えば1回目のテストでは、

  • params[0]: 1
  • params[1]: 2
  • params[2]: 3

として各値が渡されてます。


ちなみにparams[1]のようにindexで値にアクセスするのがわかりにくければ、dictで渡しても問題ありません。

var extract_words_from_text_params = [
    {"input": "abc def g", "expected": ["abc", "def", "g"]},
    {"input": "abc, def g?", "expected": ["abc", "def", "g"]},
]
func test_extract_words_from_text(params=use_parameters(extract_words_from_text_params)):
    var input = params["input"]
    var expected = params["expected"]
    var result = _obj.extract_words_from_text(input)
    assert_eq(result, expected)

こうした場合、indexアクセスではなくparams["input"]のように意味を持つkeyでアクセスさせることができます。

おわりに

以上、Godot Engineでパラメータテストする方法をメモしました。

ちょっとしたものでしたが、どなたかの参考になれば幸いです。

[関連記事]

www.bioerrorlog.work

www.bioerrorlog.work

www.bioerrorlog.work

www.bioerrorlog.work

参考

Parameterized Tests · bitwes/Gut Wiki · GitHub

Go製ゲームエンジンEbitengine入門: Boids Flockingを実装する

Boids Flockingの実装を通して、Go製のゲームエンジンEbitengineに入門します。

はじめに

Go製のゲームエンジンEbitengineというものを見つけて面白そうだったので、取り急ぎ何か作ってみようと思いました。

以前に他のゲームエンジンで実装したことのあるBoids Flockingを実装し、Ebitengineに入門していきたいと思います。

# 作業version
Go: 1.20.7
Ebitengine: v2.5.6 

Ebitengineとは

Ebitengineは、

  • Go製のオープンソース2Dゲームエンジン
  • マルチプラットフォーム対応 (Nintendo Switchもサポート)
  • ミニマムなAPI
  • 「全ては矩形画像である」- 矩形画像から矩形画像へ描画でゲームを構成する

という特徴を持ったゲームエンジンです。

ebitengine.org

設計思想はEbitengine作者様の記事にわかりやすく解説されていますので、ご興味があれば一読をお勧めします。

Ebitenginで作られた有名どころのゲーム事例としては、

などがあります。

どちらもOdencatさんからのゲーム(Nintendo Switchにも移植されてる)なのですが、OdencatさんがEbitengineを採用するに至った経緯や、このツールの活用方法を語った下記の資料も面白いのでおすすめです。

www.slideshare.net

今回作ったもの: Boids Flocking

https://github.com/bioerrorlog/boids-ebitengine/blob/main/screenshots/demo.gif?raw=true

Boids Flockingは、鳥の群れの動きをシミュレートする人工生命モデルです。 "Bird-oid"「鳥っぽいもの」という意味で、Boidsと呼ばれています。

Boids Flockingのアルゴリズム自体の説明については過去記事で整理しましたので、そちらを参照ください。

最終的なコードはGitHubに配置していますので、以降はこのコードを元にしながらポイントを整理していきます: github.com

Boids Flockingを実装する

最小構成: Hello, World!

Boids Flockingの実装に移る前に、まずはEbitengine製ゲームにおける最小構成を確認しておきます。

// main.go
package main

import (
    "log"

    "github.com/hajimehoshi/ebiten/v2"
    "github.com/hajimehoshi/ebiten/v2/ebitenutil"
)

type Game struct{}

func (g *Game) Update() error {
    return nil
}

func (g *Game) Draw(screen *ebiten.Image) {
    ebitenutil.DebugPrint(screen, "Hello, World!")
}

func (g *Game) Layout(outsideWidth, outsideHeight int) (screenWidth, screenHeight int) {
    return 320, 240
}

func main() {
    ebiten.SetWindowSize(640, 480)
    ebiten.SetWindowTitle("Hello, World!")
    if err := ebiten.RunGame(&Game{}); err != nil {
        log.Fatal(err)
    }
}

Game structを用意し、3つの関数Update Draw Layoutを実装して上記のようにRunGameに渡してあげれば、ゲームを実行することができます。

  • Updateはフレームごとに呼び出される処理で、主にゲームロジックの実行/ゲームステートの更新を担わせます。
  • Drawもフレームごとに呼び出される処理で、こちらは描画処理を担います。
  • Layoutはゲームのスクリーンサイズを定義します。

go run main.goでゲームを実行できます。

Ebitengineで Hello, World! を表示

Boids Flockingを実装する時も同じく、ロジックをUpdateに実装し、Drawで描画処理を実装する、という形をとっていきます。

ディレクトリ構成

Boids Flockingを実装するにあたって、main.goに全コードを書くと流石に読みにくくなってしまうので、下記のようなディレクトリ構成にしてみました。

# treeコマンド結果から抜粋
.
├── go.mod
├── go.sum
├── main.go
├── boids
│   ├── boid.go
│   └── game.go
└── vector
    ├── vec2.go
    └── vec2_test.go

エントリーポイントとなるmain.go、ゲームロジックを実装するboidsパッケージ、あとは二次元ベクトル操作処理を実装するvectorパッケージです。 ベクトル処理は既存のライブラリを使った方が良いのでしょうが、今回は諸々の入門として自分で書いています。

main.goは、boidsパッケージで定義するgameロジックを呼び出すだけにとどめています。

package main

import (
    "log"

    "github.com/bioerrorlog/boids-ebitengine/boids"
    "github.com/hajimehoshi/ebiten/v2"
)

func main() {
    game, err := boids.NewGame()
    if err != nil {
        log.Fatal(err)
    }
    ebiten.SetWindowSize(boids.ScreenWidth, boids.ScreenHeight)
    ebiten.SetFullscreen(true)
    ebiten.SetWindowTitle("Boids")
    if err := ebiten.RunGame(game); err != nil {
        log.Fatal(err)
    }
}

Ref. GitHub - bioerrorlog/boids-ebitengine at aa86fe9fe9d4a8626ef1d652338a5dce1e7a5f84

ゲームロジックの実装

こちらが大本となるゲームロジックです。

package boids

import (
    "fmt"
    "image/color"
    "math/rand"

    "github.com/bioerrorlog/boids-ebitengine/vector"
    "github.com/hajimehoshi/ebiten/v2"
    "github.com/hajimehoshi/ebiten/v2/ebitenutil"
)

const (
    ScreenWidth  = 1920
    ScreenHeight = 1080
    boidCount    = 100
)

type Game struct {
    boids []*Boid
}

func NewGame() (*Game, error) {
    g := &Game{
        boids: make([]*Boid, boidCount),
    }

    for i := range g.boids {
        g.boids[i] = NewBoid(
            rand.Float64()*ScreenWidth,
            rand.Float64()*ScreenHeight,
            vector.Vec2{X: ScreenWidth / 2, Y: ScreenHeight / 2},
        )
    }
    return g, nil
}

func (g *Game) Update() error {
    for _, b := range g.boids {
        b.Update(g.boids)
    }
    return nil
}

func (g *Game) Draw(screen *ebiten.Image) {
    // Backgroud
    screen.Fill(color.RGBA{255, 245, 228, 0xff})

    // Boids
    for _, b := range g.boids {
        b.Draw(screen)
    }

    // Debug
    fps := fmt.Sprintf("FPS: %0.2f", ebiten.ActualFPS())
    ebitenutil.DebugPrint(screen, fps)
}

func (g *Game) Layout(outsideWidth, outsideHeight int) (screenWidth, screenHeight int) {
    return ScreenWidth, ScreenHeight
}

Ref. boids-ebitengine/boids/game.go at aa86fe9fe9d4a8626ef1d652338a5dce1e7a5f84 · bioerrorlog/boids-ebitengine · GitHub

NewGame関数からGame structを外部から取得できるようにしています。

Game structは後述するBoidを配列で保持しており、UpdateではBoidのUpdate関数を呼び出します。

Drawでは描写を行っており、順番に背景描画、Boidsの描画、デバッグ(fps表示)描画を行っています。

Boids Flockingロジックの実装

次はGameから呼び出しているBoid個々のロジックの実装です。

package boids

import (
    "image/color"
    "math/rand"

    "github.com/bioerrorlog/boids-ebitengine/vector"
    "github.com/hajimehoshi/ebiten/v2"
    ev "github.com/hajimehoshi/ebiten/v2/vector"
)

const (
    moveSpeed                 = 20
    perceptionRadius          = 100
    steerForce                = 1
    alignmentForce            = 0.1
    cohesionForce             = 0.05
    separationForce           = 0.3
    centralizationForce       = 0.3
    centralizationForceRadius = 200
)

type Boid struct {
    position, velocity, targetCenter vector.Vec2
}

func NewBoid(x, y float64, targetCenter vector.Vec2) *Boid {
    return &Boid{
        position:     vector.Vec2{X: x, Y: y},
        velocity:     vector.Vec2{X: rand.Float64()*2 - 1, Y: rand.Float64()*2 - 1},
        targetCenter: targetCenter,
    }
}

func (b *Boid) Draw(screen *ebiten.Image) {
    ev.DrawFilledCircle(screen, float32(b.position.X), float32(b.position.Y), 20, color.RGBA{255, 148, 148, 0xff}, true)
}

func (b *Boid) Update(boids []*Boid) {
    neighbors := b.getNeighbors(boids)

    alignment := b.alignment(neighbors)
    cohesion := b.cohesion(neighbors)
    separation := b.separation(neighbors)
    centering := b.centralization()

    b.velocity = b.velocity.Add(alignment).Add(cohesion).Add(separation).Add(centering).Limit(moveSpeed)
    b.position = b.position.Add(b.velocity)
}

// 以下略

Ref. boids-ebitengine/boids/boid.go at aa86fe9fe9d4a8626ef1d652338a5dce1e7a5f84 · bioerrorlog/boids-ebitengine · GitHub

NewBoidで初期化されたBoidを返し、Drawで描画、UpdateでBoidsアルゴリズムを実行しています。

Boidsアルゴリズムとして、

  • alignment: 近隣のboidsと同じ方向に進もうとする力
  • cohesion: 近隣のboids集合の中心地に向かおうとする力
  • separation: 近隣のboidsと近づきすぎないよう距離をとる力

の3つを掛け合わせています。

それとは別に、boids達が画面外に行ってしまわないよう画面中心へと向かう力centralizationも追加しています。

それぞれの力についての詳しい実装はここでは取り上げない(上記コード// 以下略部分)ので、ご興味ある方はソースコードをご覧ください。

あとはconstで定めた各パラメータの微修正を繰り返し、なんとなく滑らかでいい感じになるようにしました。 出来上がったBoidsたちの動きは、なかなか気に入っています。

(再掲) https://github.com/bioerrorlog/boids-ebitengine/blob/main/screenshots/demo.gif?raw=true

Vector処理の実装

上記Boidの実装では、自前のvector処理を利用しています。 (本来はライブラリを使った方が良いと思いますが、せっかくの入門なので自分で書いています)

package vector

import "math"

type Vec2 struct {
    X, Y float64
}

func (v Vec2) Add(other Vec2) Vec2 {
    return Vec2{v.X + other.X, v.Y + other.Y}
}

func (v Vec2) Sub(other Vec2) Vec2 {
    return Vec2{v.X - other.X, v.Y - other.Y}
}

func (v Vec2) Mul(scalar float64) Vec2 {
    return Vec2{v.X * scalar, v.Y * scalar}
}

func (v Vec2) Div(scalar float64) Vec2 {
    return Vec2{v.X / scalar, v.Y / scalar}
}

func (v Vec2) Length() float64 {
    return math.Sqrt(v.X*v.X + v.Y*v.Y)
}

func (v Vec2) Normalize() Vec2 {
    length := v.Length()
    if length != 0 {
        return Vec2{v.X / length, v.Y / length}
    }
    return Vec2{0, 0}
}

func (v Vec2) Limit(max float64) Vec2 {
    if v.Length() > max {
        return v.Normalize().Mul(max)
    }
    return v
}

func (v Vec2) DistanceTo(other Vec2) float64 {
    diff := v.Sub(other)
    return diff.Length()
}

Ref. boids-ebitengine/vector/vec2.go at aa86fe9fe9d4a8626ef1d652338a5dce1e7a5f84 · bioerrorlog/boids-ebitengine · GitHub

Boidsアルゴリズムで使うことになる、二次元ベクトル操作の実装です。

ゲームロジックや描画の実装にはなかなかテストを書きにくいこともありますが、このような純粋なGoコードであれば普通にテストが書けます:

package vector

import (
    "math"
    "testing"
)

func TestVec2_Add(t *testing.T) {
    tests := []struct {
        name  string
        v     Vec2
        other Vec2
        want  Vec2
    }{
        {"basic add", Vec2{3, 4}, Vec2{1, 2}, Vec2{4, 6}},
        {"add with zero", Vec2{3, 4}, Vec2{0, 0}, Vec2{3, 4}},
        {"add with negative values", Vec2{3, 4}, Vec2{-1, -2}, Vec2{2, 2}},
        {"add with same values", Vec2{3, 4}, Vec2{3, 4}, Vec2{6, 8}},
        {"add with floating values", Vec2{3.5, 4.5}, Vec2{1.5, 2.5}, Vec2{5, 7}},
        {"add with mix of positive and negative", Vec2{3, -4}, Vec2{-1, 2}, Vec2{2, -2}},
    }
    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            if got := tt.v.Add(tt.other); got != tt.want {
                t.Errorf("Vec2.Add() = %v, want %v", got, tt.want)
            }
        })
    }
}

func TestVec2_Sub(t *testing.T) {
    tests := []struct {
        name  string
        v     Vec2
        other Vec2
        want  Vec2
    }{
        {"basic subtraction", Vec2{3, 4}, Vec2{1, 2}, Vec2{2, 2}},
        {"subtract with zero", Vec2{3, 4}, Vec2{0, 0}, Vec2{3, 4}},
        {"subtract negative values", Vec2{3, 4}, Vec2{-1, -2}, Vec2{4, 6}},
        {"subtract same values", Vec2{3, 4}, Vec2{3, 4}, Vec2{0, 0}},
        {"subtract floating values", Vec2{3.5, 4.5}, Vec2{1.5, 2.5}, Vec2{2, 2}},
        {"subtract mix of positive and negative", Vec2{3, -4}, Vec2{-1, 2}, Vec2{4, -6}},
        {"subtract resulting in negative", Vec2{3, 4}, Vec2{5, 6}, Vec2{-2, -2}},
    }
    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            if got := tt.v.Sub(tt.other); got != tt.want {
                t.Errorf("Vec2.Sub() = %v, want %v", got, tt.want)
            }
        })
    }
}

// 以下略

Ref. boids-ebitengine/vector/vec2_test.go at aa86fe9fe9d4a8626ef1d652338a5dce1e7a5f84 · bioerrorlog/boids-ebitengine · GitHub

テストを実行し、ALL PASSすれば安心です。

$ go test -v ./...
?       github.com/bioerrorlog/boids-ebitengine [no test files]
?       github.com/bioerrorlog/boids-ebitengine/boids   [no test files]
=== RUN   TestVec2_Add
=== RUN   TestVec2_Add/basic_add
=== RUN   TestVec2_Add/add_with_zero
=== RUN   TestVec2_Add/add_with_negative_values
=== RUN   TestVec2_Add/add_with_same_values
=== RUN   TestVec2_Add/add_with_floating_values
=== RUN   TestVec2_Add/add_with_mix_of_positive_and_negative
--- PASS: TestVec2_Add (0.00s)
    --- PASS: TestVec2_Add/basic_add (0.00s)
    --- PASS: TestVec2_Add/add_with_zero (0.00s)
    --- PASS: TestVec2_Add/add_with_negative_values (0.00s)
    --- PASS: TestVec2_Add/add_with_same_values (0.00s)
    --- PASS: TestVec2_Add/add_with_floating_values (0.00s)
    --- PASS: TestVec2_Add/add_with_mix_of_positive_and_negative (0.00s)

# 中略

PASS
ok      github.com/bioerrorlog/boids-ebitengine/vector  (cached)

ゲーム側の実装はこれで完成です。

CIの実装

最後にGitHub Actionsを設定して、コードの静的解析やbuild、testの実行を自動化させます。

name: Test

on:
  - push
  - pull_request

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v2

      - name: Setup Go
        uses: actions/setup-go@v3
        with:
          go-version: '1.20'

      - name: Install dependencies
        run: |
          sudo apt-get update
          sudo apt-get install -y libasound2-dev libgl1-mesa-dev libxcursor-dev libxi-dev libxinerama-dev libxrandr-dev libxxf86vm-dev

      - name: Run golangci-lint
        uses: golangci/golangci-lint-action@v3
        with:
          version: latest

      - name: Vet
        run: go vet ./...

      - name: Build
        run: go build -v ./...

      - name: Test
        run: go test -v ./...

Ref. boids-ebitengine/.github/workflows/test.yml at aa86fe9fe9d4a8626ef1d652338a5dce1e7a5f84 · bioerrorlog/boids-ebitengine · GitHub

必要なライブラリをインストールしたのち、静的解析(golangci-lintgo vet)、go buildgo testを実行しています。

Install dependenciesではいろんなものをインストールさせていますが、これをしないとubuntuではgo vetが落ちるので注意です。 (インストールするライブラリは本家Ebitengineのworkflowを参考にしました)

おわりに

Go製のゲームエンジンEbitengineで、Boids Flockingを実装してみました。

個人的にはこれまで、

  • 自分でゲーム作るならシンプルな2Dゲーム一択 (2Dゲーム好きだし、3D酔いしやすいので)
  • そうなると、既存のゲームエンジンはぶっちゃけ機能が過剰
    • 何かにハマったときの切り分けがつらい (徐々にモチベに悪影響)
    • (もちろん使いこなせば高い生産性が出せるのかもしれないが...)

という思いを抱いてきたので、APIがシンプルなEbitengineは触っていて心地良かったです。

以上、ちょっとした一例としてどなたかの参考になれば幸いです。

[関連記事]

www.bioerrorlog.work

www.bioerrorlog.work

www.bioerrorlog.work

参考

GitHub - hajimehoshi/ebiten: Ebitengine - A dead simple 2D game engine for Go

Ebitengine - A dead simple 2D game engine for Go

ebiten package - github.com/hajimehoshi/ebiten/v2 - Go Packages

ゲームエンジンはアートである - 8 年以上自作ゲームエンジンをメンテし続けている話|Hajime Hoshi

既に拡張性のある無料のゲームエンジンがある中,なぜ時間と労力をかけて独自のゲームエンジンを開発していらっしゃるのでしょうか? | mond

Real-world game development with Ebitengine - How to make the best-selling Go game | PPT

GitHub - bioerrorlog/boids-ebitengine: Boids flocking simulation with Ebitengine

Devlog #3 Unit testの導入と辞書機能の模索

ChatGPT x 架空言語なゲームの開発記録その3です。

前回はこちら: www.bioerrorlog.work

はじめに

前回は、ゲームにChatGPTを組み込んでテキストを生成させました。

今回は少し地味ですが、Unit testの導入と架空言語辞書機能の模索をしていたのでその記録を残します。

Devlog

Unit testの導入

これまでGodot Engineでゲームを実装するとき、テストコードを書いてきませんでした。

普段のゲーム開発ではないプログラミングではテストコードが非常に重要な位置を占めているので、これでは開発体験にも大きな違いが出てきてしまいます。

調べてみるとGodot Engineでテストを書けるフレームワークがあったので、これを導入してテストコードの拡充をしました。

www.bioerrorlog.work

後付けのテストは実装時のテストよりも効能が下がってしまうところですが、それでもコードの振る舞いについて少しづつでも安心が得られるのは良いですね。

ちなみにE2Eテスト的なものは現在手動でやっていますが (規模が全然小さいので問題ない)、将来的には自動化したいところですね。

(ゲームにおけるE2Eテスト自動化ってどうやるのだろうか..)

辞書機能の模索

このゲームのコンセプトはChatGPTを使って架空言語を話させる、というものであり、プレイヤーが言語を解読していく流れを想定しています。 言語を解読していく時には、プレイヤーによって編集可能な辞書のようなものを用意しようとしています。

この"編集可能な辞書"、さくっと書けるだろうと思ってましたが結構ハマってました (ハマってます)。 で、辞書機能の根本的な設計を見直そうとしているのですが、取り急ぎ現段階で見えている課題を将来の自分に向けて記録します。

ハリボテの辞書機能プロトタイプ。設計から見直す必要がある。

まずデータの保存ですが、これは外部ファイルに保存する、という形式にしています。 現在はjsonで保存していますが、データフォーマット/スキーマ設計は置いておいて、外部ファイルに保管する、という方針は今のところ問題なさそうです。

# 実装イメージ
func save_dict() -> void:
    var file: File = File.new()
    file.open(file_path, File.WRITE)
    file.store_string(to_json(dict))
    file.close()


あとは、プレイヤーが編集可能な辞書をどう作るか、です。

取り急ぎプロトタイプとして、Tree Nodeを使って編集可能なKey - Val辞書を作ろうとしましたが、これが少し厄介でした。 Tree Nodeでは列ごとにフォントを変えることができないので、Keyを架空言語のフォント、Valを日本語フォント、と設定を分けることができません。

全てが架空言語フォントになってしまい、意味の分からない"辞書機能"の仮実装

このフォント問題を解決(回避)しようとしてKeyとValを別々のテキストNodeとして構成すると、今度は別の問題 (編集したテキストを取得してKeyと紐付けて更新する処理などなど)に綻びが出てきてしまいます。

これら問題を切り分けするのに無駄に時間を浪費してしまいましたが、適当に仮実装で済ませようという元々の思想が悪かった、というのが今の気持ちです。

ちゃんとSceneを切り出すなどして、手を抜かずに設計し直そうと思っています。

おわりに

今回は進捗というより、試行錯誤の記録のようなものになってしまいました。

構想時は簡単だろうと思っていたものも、いざ書いてみると躓きまくる、というのはあるあるですね。

[関連記事]

www.bioerrorlog.work

www.bioerrorlog.work

www.bioerrorlog.work

Godot Engineでテストコードを書く | GUT

GUTというテストフレームワークを用いて、Godot Engineでテストコードを書く方法をまとめます。

はじめに

テストコードでコードの振る舞いを保証しながら実装を進めるのは、とても大事なことだと思っています。

が、これまでGodot Engineなどゲームエンジンで実装するときには、テストコードを書かずにきました。

今回は、GUTというGodot Engineのunit testフレームワークを使って、テストコード書く方法をまとめます。

# 作業バージョン
Godot version:  3.5.2
GUT version:  7.4.1

Godot Engineでテストコードを書く

インストール

まずは、GUTをインストールします。

GodotエディタのAssetLib (上部のタブから選べます)から、GUTを検索します。

AssetLibからGUTを検索

あとは順当に、Download -> Install ボタンを押していきます。

"Download"を選択

"Install"を選択

↓のウィンドウが表示されたら、インストール成功です。

インストール終了

GUTを有効化する

インストールができたら、次はGUTを有効化します。

エディタ上部のProjectタブからProject Settingsを選択し、Pluginsタブを選びます。 そこからGutプラグインを有効化 (Enableチェックボックスをクリック) します。

Gutプラグインを有効化する

GUTプラグインを有効化すると、エディタ下部に"GUT"タブが出現し、テスト設定/実行ができるようになります。

エディタの下部に"GUT"タブが現れる

テストコードを書く

では、いよいよテストコードを書いていきます。

テストコードを配置する場所に決まりはありませんが、下記のディレクトリが推奨されています。

  • res://test
  • res://test/unit
  • res://test/integration

テスト用スクリプトファイルはtest_から始まるファイル名で作成します。

シンプルなテストを書いてみると、こんな感じです:

extends GutTest

func before_each():
    gut.p("ran setup", 2)

func after_each():
    gut.p("ran teardown", 2)

func before_all():
    gut.p("ran run setup", 2)

func after_all():
    gut.p("ran run teardown", 2)

func test_assert_eq_number_equal():
    assert_eq('asdf', 'asdf', "Should pass")

func test_assert_true_with_true():
    assert_true(true, "Should pass, true is true")

Ref. Creating Tests · bitwes/Gut Wiki · GitHubより抜粋

まず、extends GutTestしてからコードを書きます。

テスト前後の処理を挟むには、before_each, after_each, before_all, after_allなどの関数があります。

assertには一般的なassert_eqassert_trueが使えます。

それ以外にもたくさんのutility関数やassert関数が用意されているので、詳しくは公式のwikiにあたることをお勧めします: Asserts and Methods · bitwes/Gut Wiki · GitHub


既存の関数をスクリプトからロードしてテストするには、例えば下記のようにします。

extends GutTest

var Obj = load('res://src/scripts/HUD/Dialog.gd')
var _obj = null

func before_each():
    _obj = Obj.new()

func test_convert_to_upper():
    var input = ["Hello", "world"]

    var result = _obj.convert_to_upper(input)
    var expected = ["HELLO", "WORLD"]
    assert_eq(result, expected)

loadで他のスクリプトをロードし、before_each内のnew()でインスタンス化させています。

あとは、普通にスクリプト内の関数(上記の例ではconvert_to_upper())を呼び出してテストできます。

テストを実行する

では最後にテストを実行します。

実行する前に、まずはテストコードを配置した場所 (res://test/unitなど) を"Test Directories"に指定します。

Test Directoriesを指定する

あとは、"Run All"ボタンを押せば、テストが実行されます。

"Run All"で全てのテストを実行する

テスト実行結果

追記: パラメータテスト

パラメータテストを書く方法は、別途記事にまとめました:

www.bioerrorlog.work

おわりに

以上、Godot EngineにテストフレームワークGUTをインストールし、テストを書いて実行するまでの流れを整理しました。

ゲーム開発の文面ではあまりテストが話題に上がることが少ない気がしていますが、同じくテストを書きたいどなたかの参考になれば幸いです。

[関連記事]

www.bioerrorlog.work

www.bioerrorlog.work

www.bioerrorlog.work

参考

GitHub - bitwes/Gut: Godot Unit Test. Unit testing tool for Godot Game Engine.

Gut - Godot Unit Testing - Godot Asset Library

Creating Tests · bitwes/Gut Wiki · GitHub

Asserts and Methods · bitwes/Gut Wiki · GitHub

個人開発ゲーム用のフォントを自作する

FontStructを使って個人開発ゲーム用のフォントを自作する過程をまとめます。

はじめに

最近ゲームを作っている中で、フォントを自作したくなりました。

今回はそのやり方を記録します。

個人開発ゲーム用のフォントを自作する

FontStructを使う

フォント作成には、FontStructというサービスを使いました。

FontStruct | Build, Share, Download Fonts

直感的な操作でフォントが作れるので、フォントデザインに関して一切の知識がない私でもなんとか作れそうな気がします。

料金もかからないフリーのサービスです。

著作権は問題ないのか

作成したフォントのライセンス/著作権は押さえておきたいところです。

これについては、FontStructのFAQに該当の記載があります。 

Does copyright over fonts created with FontStruct belong to me or FontStruct?

訳:FontStructで作成したフォントの著作権は、FontStructに帰属しますか? 私(フォント作成者)に帰属しますか?

You.
Copyright over fonts created with FontStruct belong to the creators, not to FontStruct.

訳:あなたに帰属します。 FontStructで作成したフォントの著作権は、FontStructではなく、フォント作成者に帰属します。

Ref. Does copyright over fonts created with FontStruct belong to me or FontStruct? | FontStruct

公式にここまで明示されていれば、問題ないでしょう。

※ ただし、私は権利/法律関係の専門家ではありません。 ライセンス/著作権関係については当記事は責任を負えませんので、ご自身の判断でよろしくお願いします。

フォントを作成する

サインアップしてアカウントを作成したら (メールアドレスによる一般的なサインアップでした)、フォントを作っていきます。

1. "My FontStruct"タブから、"New FontStruction"をクリックします。


2. "Name your design"欄にフォント名を入力し、"Start FontStructing"ボタンをクリックすると、フォント編集画面に移ります。


3. フォントの編集画面はこんな感じです。 あとは自由にポチポチお絵描きして、フォントを作成していきます。

フォントの編集画面

フォントの編集画面 (編集中)


4. フォントを作成し終わったら、フォントファイルをダウンロードします。 編集画面から"Download"ボタンをクリックすると、ttfファイルがダウンロードされます。

作成した架空言語風のフォント

これで自作フォントのttfファイルが手に入ります。

おわりに

以上、フォントを作ってみる過程の記録でした。

作ったフォントをゲームに適用してみた絵は、こんな感じです:

どなたかの参考になれば幸いです。

[関連記事]

www.bioerrorlog.work

www.bioerrorlog.work

www.bioerrorlog.work

参考

FontStruct | Build, Share, Download Fonts

Does copyright over fonts created with FontStruct belong to me or FontStruct? | FontStruct

Terms of Use | FontStruct

Devlog #2 ゲームにChatGPTを組み込む

実際にゲームにChatGPTを組み込みます。

前回はこちら: www.bioerrorlog.work

はじめに

前回は、過去作をベースにテキストベースのシステムを導入し、架空言語 x ChatGPTという組み合わせのゲームを作る下地を作りました。

今回はそこに、実際にChatGPTを組み込んでいきます。

Devlog

設定画面の実装

ChatGPT APIをcallするためのAPI keyを入力&保存する機能を作る前提として、まずは設定画面を実装します。

  1. 設定画面を用意する
  2. 設定画面とメイン画面を切り替えるボタンを用意する

こう書いてみると単純ですが、意外と躓きました。

まず、(簡易的な)設定画面を用意するのは比較的簡単です。 設定画面用のSceneを作成し、そこに背景やらボタンやらを配置します。

本当はもう少し階層化させた方がいいのかもしれない

面倒だったは、"2. 設定画面とメイン画面を切り替えるボタンを用意する"の方です。

最初は、特定のボタンを押すと実行SceneがMainから設定画面Sceneに切り替わる、のようにしてましたが、これがなかなか上手くいきません。

  • 設定画面SceneからMain sceneに戻すと、Scene状況が初期化されている
  • それを防ぐために、Scene切り替え時に実行Sceneを一時停止しようとする
  • それも上手くいかず..

みたいなことを繰り返していました。

結局、元から設定Sceneも配置しておいて、visibilityをボタンで制御する、という方法にしました。

# 実装イメージ

extends CanvasLayer

onready var settings = $Settings
onready var settings_icon = $SettingsIcon


func go_to_settings() -> void:
    settings.visible = true
    settings_icon.visible = false


func close_settings() -> void:
    settings.visible = false
    settings_icon.visible = true


func _on_SettingsIcon_settings_icon_pressed() -> void:
    go_to_settings()


func _on_Settings_close_button_pressed() -> void:
    close_settings()

よほどシンプルになりました。

ただ、設定画面を開いているときはMain Sceneを停止する、とかしたいのであればもう少し工夫が必要そうです。 が、今は一旦これでよしとします。

API Keyの入力と保存

次は設定画面で、OpenAI API Keyの入力とその保存機能を実装します。

このあたりの話は、以前書いた↓の記事とほぼ同様のことを実装していますので、割愛します。

www.bioerrorlog.work

ChatGPTにセリフを出力させる

OpenAI API Keyを保存させることができたので、いざキャラのセリフをOpenAI API経由で生成させてみます。

今回のコンセプトはChatGPT x 架空言語です。 本来はちゃんとした言語設計とそれをChatGPTに喋らせるためのプロンプト設計が必要ですが、今回は仮でまずChatGPT API経由で何かしらの適当な文字列を出力させます。

その時の動画がこちら↓

最初の2文があらかじめ定義しておいたセリフ、以降の文がChatGPT APIに生成させたセリフです。

思ったよりChatGPT APIからのレスポンスが早くなっていました (以前はもっと遅かった)。

全文をChatGPT APIに生成させても、そこまで違和感ないスピードで表示できるかもしれません。

補足: カメラフォーカスの修正

地味ですが、ズーム時にカメラが他オブジェクト(キャラクター)にフォーカスするようにもしています。

前は単純にオブジェクト接触時にカメラのZoomを上げるようにしていたので、フォーカスはPlayerのままでした。 これを、Zoom時にPlayerとオブジェクトの座標差を取得してカメラオフセットに入れることで、カメラフォーカスをPlayerからオブジェクトにずらすようにしました。

# 実装イメージ
func focus_on_other_object(other_object: Node2D, duration: float = 0.2) -> void:
    var new_offset: Vector2 = other_object.global_position - self.global_position
    tween.interpolate_property(camera, "offset", camera.offset, new_offset, duration, Tween.TRANS_LINEAR, Tween.EASE_IN_OUT)
    tween.start()

Zoom時のカメラフォーカスをPlayerではなくオブジェクトにする

ただ、なんかカメラ移動とフォーカスの動きが微妙にぎこちないので、その辺りは要改善ですね。

おわりに

以上、今回はOpenAI API Keyの保存周りの諸々と、ChatGPT APIを実際に組み込むところをやりました。

次回は、実際に架空言語を設計してみるあたりでしょうか。 ただ、言語を設計するならそもそもゲームがどう始まってどう終わるのか、というゲームの筋書きも置く必要がありますね。

少しずつやっていきます。

次回はこちら:

www.bioerrorlog.work

[関連記事]

www.bioerrorlog.work

www.bioerrorlog.work

www.bioerrorlog.work

Devlog #1 ChatGPTを使ったゲームを作る

ゲーム開発の記録を残します。

はじめに

久しぶりにゲームでも作ってみようか、という機運が高まっています。

ChatGPTを触って衝撃を受けてから、真っ当な活用方法以外にも何かしら面白い使い道はないだろうか、と模索したり妄想したりする日々です。
最近Raspberry PiとChatGPTを組み合わせる実験をしたのですが、これもなかなか面白いものでした。

www.bioerrorlog.work

ChatGPTの面白さを活かすには、やはりゲームも一つの大きな選択肢に思います。

幸い私は、過去に2つほどミニゲームのようなものを作ったことがあります。

ChatGPTのAPIをGodot Engineから呼び出すのもかなりシンプルに実装できることを確認したので、過去作をベースに何かしら作ってみようと思います。

いつ飽きるかは分かりませんが、せっかくなのでDevlog的な記録を残していくことにします。

Devlog

まずは過去作をベースにする

ゼロからゲームを作る、というのは骨が折れます。

まずはプロトタイプ的にサクッと作ってみるなら、自分の過去作をベースにするのが楽でしょう。 私がこれまでに書いたゲームは、一つ目がPygame、二つ目がGodot Engineで作られています。

Godot Engineの方がなんとなく肌に合っていると感じたので、そちらのゲームを出発点にすることにしました。

Unityも何度か触ったことがあるのですが、いまいち使いにくい気がして長続きしませんでした。

↑こんな感じの、"Boids Flocking"をテーマとした避けゲーです。

だいぶ昔に実装した覚えがあるのですが、辿ってみたらたかが3年前くらいのものでした。

当時書いた記事はこちら:

www.bioerrorlog.work

テキストベースのゲームシステムを導入する

ChatGPTを活用するなら、何かしらテキストベースのシステムを実装するのが良さそうです。

コードをいじりながらGodot Engineの感覚を思い出しつつ、テキストダイアログを実装してみたのがこちら↓

  • NPCキャラクターを配置する
  • Playerがキャラクターに接触すると、カメラがズームする
  • Playerがキャラクターに接触すると、Dialogが開く
  • Playerがキャラクターに接触すると、Playerは移動不可になる
  • PlayerはDialog終了後に移動可能になる

みたいなところを実装してみました。

ゲームエンジンを触るのは久しぶりですが、純粋に楽しいですね。

架空言語 x ChatGPTという組み合わせ

さて、何かChatGPTの面白い使い方はないだろうか、と思ったところで、架空言語をChatGPTに喋らせることはできないだろうか、という考えが浮かびました。

取り急ぎChatGPTは使わないまま、架空言語っぽいものを表示してみる、をやってみたのがこちら↓

独自フォントを作成し、架空言語のように仕立てています。

あと地味ですが、文字を徐々に表示させるようにしたことで少しそれっぽくなりました。

※ フォントを自作するやり方は、別途記事にしました:

www.bioerrorlog.work

おわりに

今回はここまで。

次は架空言語の単語辞書の用意と、それをChatGPTに喋らせる仕組みの実装でしょうか。

気が向いたときに、気楽にやっていきます。

次回はこちら: www.bioerrorlog.work

[関連記事]

www.bioerrorlog.work

www.bioerrorlog.work

www.bioerrorlog.work

ChatGPTをGodot Engineから呼び出す

ChatGPT / OpenAI APIをGodot Engineのゲームから呼び出す実装の備忘録です。

はじめに

ゲーム分野は、昨今のLLM/生成AIの発展に大きな恩恵を受ける分野の一つだと思います。 ChatGPTを利用してどんなゲームができるだろうか...と、考えるだけでも楽しいですね。

今回は、Godot EngineからChatGPT / OpenAI APIを呼び出すシンプルな実装をやってみます。

# 作業環境
Godot Engine v3.5.2.stable.official.170ba337a

ChatGPTをGodot Engineから呼び出す

最終的なプロジェクトのコードはこちらに配置しています。

github.com

概要

ゲーム画面

構成はシンプルです。

  1. プレイヤーはOpenAI API Keyを入力する
    -> API Keyがローカルに保存される
  2. プレイヤーはメッセージを入力して"Talk"ボタンを押下
    -> OpenAI APIがcallされる
  3. 返答が画面に反映される

順を追ってポイントを説明していきます。

詳細はソースコードを参照ください。

API Keyの保存とロード

まずは、プレーヤーがOpenAI API Keyを入力し、それをセーブ&ロードできるようにしておきます。

extends Node

var save_file: ConfigFile = ConfigFile.new()

func save_api(key: String) -> void:
    print("Start saving api key")
    save_file.set_value("credentials", "api_key", key)
    save_file.save("user://credentials.cfg")
    print("Save api key finished")

func load_api() -> String:
    print("Start loading api key")    
    var error = save_file.load("user://credentials.cfg")
    if error == OK:
        var api_key = save_file.get_value("credentials", "api_key")
        print("Load api key finished")
        return api_key
    else:
        print("Error: Loading api key failed")
        return ""

call-gpt-godot/SaveManager.gd at main · bioerrorlog/call-gpt-godot · GitHub

ConfigFileを使って、API Keyをローカルに保存、およびロードする機能を用意しておきます。 ちなみに保存ディレクトリを指定するときに使っているuser://が指し示している場所については、別途記事にまとめたのでこちらを参照ください。

`user://`はどこを指しているのか | Godot Engine - BioErrorLog Tech Blog

あとはこれを画面上のユーザー入力/ボタンと接続して、API Keyをゲーム画面から保存できるようにします。

# 一部抜粋
extends Node2D

onready var save_manager = preload("res://src/SaveManager.gd").new()

func _on_SaveButton_pressed():
    save_manager.save_api(save_input.text)

call-gpt-godot/Main.gd at main · bioerrorlog/call-gpt-godot · GitHub

OpenAI APIを呼び出す

OpenAI APIを呼び出す部分の実装は、HTTPRequestを使って実装しています。

extends HTTPRequest

signal response_received(response)

var openai_url = "https://api.openai.com/v1/chat/completions"

func _init():
    connect("request_completed", self, "_on_request_completed")

func call_api(api_key: String, message: String, max_tokens: int = 100):
    var headers = [
        "Authorization: Bearer " + api_key,
        "Content-Type: application/json",
    ]

    var postData = {
        "model": "gpt-3.5-turbo",
        "messages": [{"role": "user", "content": message}],
        "max_tokens": max_tokens
    }

    var error = self.request(openai_url, headers, false, HTTPClient.METHOD_POST, JSON.print(postData))
    if error != OK:
        print("HTTP request failed with error: ", error)

func _on_request_completed(result, response_code, headers, body):
    var response = JSON.parse(body.get_string_from_utf8())
    if "choices" in response.result and response.result["choices"].size() > 0:
        emit_signal("response_received", response.result.choices[0].message.content.split("\n"))
    else:
        emit_signal("response_received", [])

call-gpt-godot/OpenAIApi.gd at main · bioerrorlog/call-gpt-godot · GitHub

基本的にOpenAI APIの仕様に従ってAPI callを行っているだけです。

この実装では、ユーザーの入力をuser roleとしてChatCompletionのAPIにPOSTしています。 よりプロンプトを工夫する場合は、この部分のプロンプト設計を練っていきます。

APIからの返答を処理した後にemit_signalすることで、外部からこのcall_apiを呼び出して結果を受け取れるようにしています。

返答を画面に反映させる

あとは、APIの返答を受け取って画面に反映させます。

ここまでに挙げたセーブ機能SaveManager.gdやOpenAI API call機能$OpenAIApiを呼び出すMainスクリプトがこちらです。

extends Node2D

var dialogues = []
var dialogue_index = 0

onready var dialogue_label = $CanvasLayer/Dialogue
onready var user_input = $CanvasLayer/UserInput
onready var save_input = $CanvasLayer/SaveInput
onready var openai_api = $OpenAIApi

onready var save_manager = preload("res://src/SaveManager.gd").new()

func _ready():
    openai_api.connect("response_received", self, "_on_response_received")
    display_dialogue()

func display_dialogue():
    if dialogue_index < dialogues.size():
        dialogue_label.text = dialogues[dialogue_index]
    else:
        print("End of the dialogue")

func talk(user_text: String):
    var api_key = save_manager.load_api()
    openai_api.call_api(api_key, user_text)

func _on_TalkButton_pressed():
    talk(user_input.text)

func _on_SaveButton_pressed():
    save_manager.save_api(save_input.text)

func _on_response_received(response: Array):
    dialogues = response
    dialogue_index = 0
    display_dialogue()

call-gpt-godot/Main.gd at main · bioerrorlog/call-gpt-godot · GitHub

_on_response_receivedでAPIからの返答を受け取り、ダイアログに反映させます。

ダイアログに文字列を表示するあたりの実装は割と適当に(ChatGPTとペアプロしながら)サクッと書いただけなので、よりちゃんとしたゲームにする場合はちゃんとした設計が必要になるでしょう。

おわりに

以上、Godot EngineからChatGPTを呼び出す最小限のやり方をメモしました。

もともとはLangChainとかを使いたかったので、Godot EngineからPythonコードを呼び出すやり方を実験していました:

GitHub - bioerrorlog/langchain-godot: Call LangChain from the Godot Engine project

ただこの場合、ExportにPython仮想環境を丸っと含めて...とか色々と面倒なステップが挟まるので、可能ならシンプルにGDScriptからHTTP callしたほうが良さそうだな、と思うに至ります。

既存ライブラリのパワーをフル活用したい、とかでなければ、GDScriptで頑張るやり方でも何かしら面白い試みができるんじゃなかろうか、と思っています。

[関連記事]

www.bioerrorlog.work

www.bioerrorlog.work

www.bioerrorlog.work

参考

GitHub - bioerrorlog/call-gpt-godot: Call OpenAI API from Godot Engine

HTTPRequest — Godot Engine (stable) documentation in English

OpenAI API

ConfigFile — Godot Engine (stable) documentation in English

`user://`はどこを指しているのか | Godot Engine - BioErrorLog Tech Blog

OpenAI API

GitHub - bioerrorlog/langchain-godot: Call LangChain from the Godot Engine project

`user://`はどこを指しているのか | Godot Engine

Godot Engineにおいて、user://で指定されるディレクトリがどこなのかの備忘録です。

はじめに

ConfigFileを使ってデータをsaveする際、user://で指定するディレクトリにファイルを保存することがあります。

# 例

# Create new ConfigFile object.
var config = ConfigFile.new()

# Store some values.
config.set_value("Player1", "player_name", "Steve")
config.set_value("Player1", "best_score", 10)
config.set_value("Player2", "player_name", "V3geta")
config.set_value("Player2", "best_score", 9001)

# Save it to a file (overwrite if already exists).
config.save("user://scores.cfg")

このuser://が具体的にはどのパスなのかが分からなかったので、備忘録にまとめます。

user:// はどこを指しているのか

デフォルトでは各プラットフォームそれぞれ下記のディレクトリを指しています。

# Windows
%APPDATA%\Godot\app_userdata\[project_name]

# macOS
~/Library/Application Support/Godot/app_userdata/[project_name]

# Linux
~/.local/share/godot/app_userdata/[project_name]

参考:ドキュメントより


ちなみにこのディレクトリパスは、カスタムすることも可能です。

Godot Engineのエディターから、
Project Settings > Application > Config > Custom User Dir Name
を指定することで、下記のディレクトリ配下に自由にパスを指定することができます。

# Windows
%APPDATA%\[custom_user_dir_name]

# macOS
~/Library/Application Support/[custom_user_dir_name]

# Linux
~/.local/share/[custom_user_dir_name]

Godot Engineエディターから、Project Settings > Application > Config > Custom User Dir Name を指定する

おわりに

以上、Godot Engineで指定できるuser://の場所をメモしました。

どなたかの参考になれば幸いです。

[関連記事]

www.bioerrorlog.work

www.bioerrorlog.work

www.bioerrorlog.work

参考

ConfigFile — Godot Engine (stable) documentation in English

Where are "user://" locations on each platform? - Godot Engine - Q&A

File paths in Godot projects — Godot Engine (stable) documentation in English

“Failed to execute script XX” エラー対処: PyinstallerでPygameをexe化するときの注意点

PyinstallerでPygameスクリプトをexe化する際に発生した、以下のエラーに対処するためのチェックポイントを書き残します。

Failed to execute script XX

はじめに

こんにちは、@bioerrorlogです。

以前、Pygameで作ったゲームのスクリプトをPyinstallerを用いてexe化し、itch.ioで配布してみました。


その際、何度か悩まされたエラーがこちらです。

Failed to execute script XX

今回は、このエラーが発生する状況とその対処方法を書き残します。

環境

Windows10

各バージョン情報

Python 3.7.6
pygame 2.0.1
pyinstaller 4.2

“Failed to execute script XX” エラーの対処法

私がエラーに遭遇した状況からまとめると、以下の3つの注意点があります。

  1. Pygameスクリプト内のimportパッケージはインストールされているか
  2. ゲームアセットの依存関係は保たれているか
  3. Pygame終了時にsys.exit()しているか

以下、ひとつずつ説明します。

Pygameスクリプト内のimportパッケージはインストールされているか

まず一つ目、Pygameをexe化する際には、そのスクリプト内でimportされているパッケージがインストールされている必要があります。

例えばGitHubからコードをcloneし、そのままPyinstallerによるexe化を行った場合、ゲームの実行に必要なパッケージがインストールされていないままの可能性があります(私がそうでした)。

必要なパッケージがインストールされないまま作成されたexeファイルは、実行時にエラーが発生します。

対処法:

  • Pyinstallerによるexe化を行う前に、ゲームが正常に実行できるかを確認する
  • ゲームが実行できなかった場合、必要パッケージをインストールする

ゲームアセットの依存関係は保たれているか

作成されたexeファイルとゲームアセットファイルの間には、Pygameスクリプトとゲームアセットファイルの関係が保たれている必要があります。

例えば、以下のようなファイル構成のPygameプロジェクトがあったとします。

.
├── main.py # Pygameスクリプト
└── data # ゲームアセットフォルダ

main.pyから、dataフォルダは以下に配置された画像データ/音声データなどを参照しているような形です。

こちらをPyinstallerでexe化すると、次のように各ファイルが生成されます。

.
├── build
├── main.py
├── main.spec
├── data
├── __pycache__
└── dist
    └── main.exe # 実行ファイル

ここでmain.exeをそのまま実行しても、エラーを吐かれてしまいます。 アセットフォルダdatamain.exeと同階層にないため、アセットを読み込むことが出来ないからです。

よって、アセットフォルダdataを、main.exeと同階層にコピーする必要があります。

└── dist
    ├── data # コピーしてくる
    └── main.exe

対処法:

  • exeファイルとゲームアセットフォルダの関係を、ゲームアセットフォルダとPygameスクリプトの関係で保持する
  • 例) ゲームアセットフォルダをexeファイルの同階層にコピーする

Pygame終了時にsys.exit()しているか

ゲーム終了時には、sys.exit()を実行する必要があります。 sys.exit()が実行されない場合、exeファイルから実行したゲームを終了する際、エラーを吐かれてしまいます。

例えばゲーム終了の関数を用意する場合は、次のようにsys.exit()を実行するようにします。

import pygame
import sys

def quit_game():
    pygame.quit()
    sys.exit()

対処法:

  • ゲーム終了時の処理でsys.exit()を実行する

おわりに

以上、PyinstallerでPygameスクリプトをexe化する際に発生したエラーへの対処法を3つ書きました。

エラーメッセージもあまり詳しく書かれないので、単純な原因でもエラーの対処に結構時間がかかりました。

Pygameでのゲーム開発はマイナーとは思いますが、同じ境遇のどなたかの参考になれば幸いです。

[関連記事]
www.bioerrorlog.work

www.bioerrorlog.work

参考

Making an Executable from a Pygame Game (PyInstaller) - YouTube

python - "Failed to execute script myscript" when exiting pygame window without console open after converting to .exe with pyinstaller - Stack Overflow

Pygameのexeファイルを作成する | PyInstaller

PygameスクリプトをPyinstallerを用いてexe化する方法の備忘録です。

はじめに

おはよう。@bioerrorlogです。

Pygameで簡単なゲームを自作してみました。

初心者がPythonでゼロからゲームを作ってみた | デザインから実装まで - 生物系がゼロから始めるTech Blog

せっかく作ったのだからどこかでゲーム実行ファイルを配布しようと思い、itch.ioにゲームを登録しました。

ゲームを配布するには、ソースコードからゲーム実行ファイル(exeファイル)を作成する必要があります。

そこで今回は、PygameスクリプトをPyInstallerを用いてexe化するやり方を残します。

環境

Windows10で作業しました。

各バージョン情報は以下です。

Python 3.7.6
pygame 2.0.1
pyinstaller 4.2

PyInstaller でPygameのexeファイルを作成する

PyInstaller でPygameのexeファイルを作成するには、以下の手順を踏みます。

  1. PyInstallerのインストール
  2. exeファイルの作成
  3. ゲームアセット依存関係の解決

以下、ひとつひとつ説明します。

1. Pyinstallerのインストール

まずは、Pyinstallerをpipインストールします

pip install pyinstaller

終わったら、バージョンを確認してPyinstallerが正常にインストールされていることを確認します。

$ pyinstaller --version
4.2

無事バージョンが表示されれば、インストール完了です。

2. exeファイルの作成

続いて、Pygameプロジェクトのソースコードからexeファイルを作成します。

今回は、私のPygameプロジェクトを例にexe化してみます。

GitHub - bioerrorlog/CellForRest_Pygame: My first project - Clicker game in Python.

# Pygameプロジェクトを手元にclone
git clone https://github.com/bioerrorlog/CellForRest_Pygame.git
cd CellForRest_Pygame/game/

このPygameプロジェクトは、以下のようなフォルダ構成になっています。

.
├── CellForRest.py
└── data

CellForRest.pyがメインの実行ファイルで、dataフォルダの中に、画像などのゲームアセットファイルが格納されています。

これらを、以下のPyInstallerコマンドでexe化します。

pyinstaller CellForRest.py --onefile --noconsole

--onefileオプションと--noconsoleオプションは、必要に応じて付与してください。 それぞれ以下の処理が行われます。

  • --onefile
    関連ファイルを1つにまとめてexe化する

  • --noconsole
    exeファイル実行時にコンソールを非表示にする

上記コマンドを実行すると、もともとのディレクトリに以下のようなフォルダが生成されます。

.
├── build
├── CellForRest.py
├── CellForRest.spec
├── data
├── dist
└── __pycache__

生成されたフォルダの中のdistフォルダの中に、exeファイルが作成されています。

└── dist
    └── CellForRest.exe


[関連記事]
www.bioerrorlog.work

3. ゲームアセット依存関係の解決

最後に、exeファイルとゲームアセットフォルダの依存関係を解決します。

もともと、メインスクリプトCellForRest.pyと同階層にゲームアセットフォルダdataを配置していたので、生成したexeファイルでも同様の依存関係を保持する必要があります。

つまり今回の例だと、dataフォルダをdistフォルダ内にコピーします。

└── dist
    ├── data # コピーしてくる
    └── CellForRest.exe

これで、exeファイルCellForRest.exeを実行すれば、ゲームが起動するようになりました。

ゲームを配布するときは、このdistフォルダの中身(dataフォルダとCellForRest.exe)をzipする形になります。

おわりに

今回は、Pyinstallerを用いてPygameのexeファイルを作成する方法を書きました。

私の場合、exeファイルとゲームアセットフォルダの依存関係を保持する必要があることを知らなかったため、しばらくexeファイルが上手く実行できずに苦労してしまいました。

同じ境遇の誰かの参考になれば幸いです。

[関連記事]
www.bioerrorlog.work

www.bioerrorlog.work

参考

Making an Executable from a Pygame Game (PyInstaller) - YouTube

PyInstaller Manual — PyInstaller 4.2 documentation

Using PyInstaller — PyInstaller 4.2 documentation

pyinstaller · PyPI

Unityで位置情報(緯度/経度/高度)を取得する | Android / iOSアプリ

UnityでGPS位置情報(緯度/経度/高度)を取得する方法を整理します。


はじめに

こんにちは、@bioerrorlogです。

ふと、位置情報を利用したアプリを作ってみたいと思い立ちました。

今回はその第一歩目として、Unityを用いて位置情報を取得する方法を整理します。


Unityバージョン:
2019.4.13.f1 LTS

Unityで位置情報を取得する

最終的なアプリ

最終的なUnityプロジェクトのソースコードは以下に置いています。

github.com

以下のように、10秒毎にスマホ(Android / iOS)から緯度/経度/高度を取得し、画面出力するシンプルなアプリケーションです。

スマホから緯度/高度/経度を取得し、テキスト表示する

以下、要点を説明します。

位置情報を取得するスクリプト

位置情報を取得するC#スクリプトを以下に示します。
位置情報の取得には、UnityのLocationService機能を利用します。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Location : MonoBehaviour
{
    public static Location Instance { set; get; }

    public float latitude;
    public float longitude;
    public float altitude;

    private void Start()
    {
        Instance = this;
        DontDestroyOnLoad(gameObject);
        StartCoroutine(StartLocationService());
    }

    private IEnumerator StartLocationService()
    {
        // First, check if user has location service enabled
        if (!Input.location.isEnabledByUser)
        {
            Debug.Log("GPS not enabled");
            yield break;
        }

        // Start service before querying location
        Input.location.Start();

        // Wait until service initializes
        int maxWait = 20;
        while (Input.location.status == LocationServiceStatus.Initializing && maxWait > 0)
        {
            yield return new WaitForSeconds(1);
            maxWait--;
        }

        // Service didn't initialize in 20 seconds
        if (maxWait <= 0)
        {
            Debug.Log("Timed out");
            yield break;
        }

        // Connection has failed
        if (Input.location.status == LocationServiceStatus.Failed)
        {
            Debug.Log("Unable to determine device location");
            yield break;
        }

        // Set locational infomations
        while (true) {
            latitude = Input.location.lastData.latitude;
            longitude = Input.location.lastData.longitude;
            altitude = Input.location.lastData.altitude;
            yield return new WaitForSeconds(10);
        }
    }
}

コードは、Unityドキュメントに記載されているサンプルコードを参考・改変したものです。

流れとしては、コルーチンの中で、

  • GPSアクセス権限の確認
  • LocationServiceの起動
  • LocationService起動失敗時の処理
  • LocationServiceから位置情報の取得

を行っています。


        // First, check if user has location service enabled
        if (!Input.location.isEnabledByUser)
        {
            Debug.Log("GPS not enabled");
            yield break;
        }

まず、Input.location.isEnabledByUserを参照し、スマホの位置情報へのアクセスが許可されているかを確認します。 アクセスが許可されていない場合は、yield break;でコルーチンを終了します。


        // Start service before querying location
        Input.location.Start();

        // Wait until service initializes
        int maxWait = 20;
        while (Input.location.status == LocationServiceStatus.Initializing && maxWait > 0)
        {
            yield return new WaitForSeconds(1);
            maxWait--;
        }

次に、Input.location.Start();でLocationServiceを起動します。 最大で20秒間の起動時間を設け、LocationServiceの起動を待機します。

起動待機の実装としては、yield return new WaitForSeconds(1);とすることで1秒後にコルーチンを再開させ、LocationServiceのステータスを監視させています。


        // Service didn't initialize in 20 seconds
        if (maxWait <= 0)
        {
            Debug.Log("Timed out");
            yield break;
        }

        // Connection has failed
        if (Input.location.status == LocationServiceStatus.Failed)
        {
            Debug.Log("Unable to determine device location");
            yield break;
        }

次は、LocationServiceの起動に失敗した時の処理を入れています。 20秒経ってもLocationServiceが起動しなかったときmaxWait <= 0、LocationServiceのステータスがFailedとなったときInput.location.status == LocationServiceStatus.Failedに、それぞれコルーチンを終了させます。


        // Set locational infomations
        while (true) {
            latitude = Input.location.lastData.latitude;
            longitude = Input.location.lastData.longitude;
            altitude = Input.location.lastData.altitude;
            yield return new WaitForSeconds(10);
        }

最後に、いよいよ緯度/経度/高度を取得します。 それぞれ、Input.location.lastData.から緯度latitude / 経度longitude / 高度altitudeを取得します。

今回は数秒毎に定期で取得したかったので、whileループの中で位置情報取得毎に10秒の待機を挟んでいます。


以上で、位置情報が取得出来ました。

この位置情報を外部から参照するには、上記スクリプトで作成したクラスLocation内で作成したInstanceを介して参照します。

例えばTextとして位置情報を表示するためには、以下のようなスクリプトを別途立てて、Location.Instanceから位置情報を参照します。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public class UpdateLocationText : MonoBehaviour
{
    public Text location;

    private void Update()
    {
        location.text = $"緯度: {Location.Instance.latitude}\n経度: {Location.Instance.longitude}\n高度: {Location.Instance.altitude}\n\nCount: {Location.Instance.gps_count}\nMessage:\n{Location.Instance.message}";
    }
}

詳しくは、以下ソースプロジェクトを参照ください。

github.com

おわりに

今回は、Unityを用いて位置情報の取得方法の備忘録を書きました。

スマホ端末のGPS位置情報機能をこのように手軽に利用できるのは、Unityの強いところだと感じます。 (Godot Engineでは位置情報にアクセスする機能はまだ提供されていないようです)

この機能を使って、なにか位置情報アプリでも作れたらと思っています。

[関連記事]

www.bioerrorlog.work

www.bioerrorlog.work

参考

Unity - Scripting API: LocationService

Unity - Scripting API: LocationInfo

Unity - Manual: Coroutines

Unity Mobile GPS - Real World Location - Unity 3D [Tutorial] - YouTube