📚

GDScript と tscn を Tree-sitter で解析して、Asciidoctor で UML 入りドキュメントを自動生成する。

に公開

Godot Engine Advent Calendar 2025 の 24 日目の記事です。

きっかけ

みなさんは Godot でプロジェクトを作成していて、これってどうやって動いているんだっけ? と、全体像がわからなくなることはありませんか?

私はよくわからなくなるので、PlantUML でクラス図を作ることがあります。

ただ、書くのがめんどくさかったり、メンテし続けるのが億劫になるので、
ER図の自動生成みたいに、ドキュメントを自動生成したくなりました。

Godot のシーンファイルである *.tscn は、テキストファイルなので、これをうまいことパースしてドキュメントを自動生成出来ないかな。と思って、作ってみました。

できたもの

gd-doc というものを作成しました。
https://github.com/tkmfujise/gd-doc

動かしてみた動画

https://youtu.be/ieUu7soeRn4

デモサイト

ゲーム開発初心者がGodotで2Dプラットフォームを勉強用にクローンする制作日誌』 で作成中の Godot プロジェクトに対して、ドキュメントを自動生成して、GitHub Pages にデプロイしてみました。
https://tkmfujise.github.io/Platformer-Game-Learning/

使用技術

全体像

以下、処理概要です。

  1. $ gd-doc install で、Nanoc で動作する構成のテンプレートをコピーして配置。(赤のライン)
  2. 上記で作成されたディレクトリ上で、$ rake コマンドを実行してドキュメントを生成。
    1. Godot のプロジェクト配下のファイルを読み込んで、Tree-sitter でパース、AsciiDoc ファイルを生成。(緑のライン)
    2. NanocAsciidoctor を呼び出して HTML に変換。その後、Web サーバを起動。(青のライン)
  3. http://localhost:3001 にアクセスしてドキュメントを閲覧。
    1. PlantUML の埋め込みコードは Asciidoctor<img src="https://kroki.io/xxx" /> に変換しているので、ブラウザ経由で Kroki にアクセスして図を動的に生成。

アーキテクチャ図

導入方法

0. Ruby と Tree-sitter をインストールする

Ruby のインストールは割愛。

Tree-sitter は、以下コマンドでインストールします。

Ubuntu (Windows WSL) の場合
$ sudo apt install libtree-sitter-dev

macOS の場合
$ brew install tree-sitter-cli

1. GitHub から gd-doc リポジトリをクローンする

どこか適当なところに落としてください。
ただし、このディレクトリは gd-doc コマンドのためにずっと使うので、Downloads とか適当すぎるところは避けて、管理しやすい場所がよいと思います。

$ git clone --depth 1 --recurse-submodules git@github.com:tkmfujise/gd-doc.git

$ cd gd-doc

2. $ bundle install で Gem をインストールする

Ruby をインストールしていない人は、要インストール。

Gem をインストールします。

$ bundle install

3. $ rake build で GDScript と GodotResource の Tree-sitter のパーサをビルドする

Tree-sitter をインストールしていない人は要インストール。手順.0 参照

GDScript (.gd) と GodotResource (.tscn, .tres, .godot) をパースできるようにするため、Tree-sitter をビルドします。

$ rake build

$ ls tree-sitters/
Ubuntu (Windows WSL) の場合
gdscript.so  godot-resource.so

macOS の場合
gdscript.dylib  godot-resource.dylib

4. $ gd-doc コマンドを使えるように PATH を通す

$ gd-doc コマンドをどこからでも呼べるようにするため、bin/gd-doc のシンボリックリンクを作成します。

$ ln -s $(realpath bin/gd-doc) ~/.local/bin/gd-doc
もしくは
$ ln -s $(realpath bin/gd-doc) /usr/local/bin/gd-doc

以下のように表示できたら成功です。

$ gd-doc --help
Commands:
  gd-doc install [DIRECTORY]    # Install docs
  gd-doc repl                   # Start REPL
  gd-doc update                 # Update docs
  gd-doc upgrade                # Upgrade gd-doc

ドキュメントの生成方法

$ gd-doc コマンドのセットアップが終われば、後は簡単です。

$ cd your_godot_project
$ gd-doc install
$ cd gd_doc
$ bundle install ※ `gd-doc install` の実行自体が初回の場合のみ。一度入れた後は不要。
$ rake

で、ドキュメントが生成できます。

以下、順に説明します。

1. $ gd-doc install コマンドを実行

Godot プロジェクトに移動して、$ gd-doc install コマンドを実行します。

$ cd your_godot_project

$ gd-doc install

解説

$ gd-doc install コマンドを実行すると gd_doc ディレクトリが作成されます。

$ ls gd_doc/
Gemfile       Rules      layouts     tmp
Gemfile.lock  config.rb  lib
Rakefile      content    nanoc.yaml

これは、Nanoc の構成ファイルに、以下の 2 ファイルが加わったものになります。

  • Rakefile
  • config.rb

Rakefile には、
AsciiDoc ファイルの生成
→ HTMLファイルへのコンパイル
→ サーバの起動
という一連の処理を順に行なうタスクを、デフォルトタスクとして定義しています。

Rakefile
desc 'Update *adoc files and compile, then run server'
task :default => %i[update compile server]

desc 'Update *.adoc files'
task :update do
  sh 'gd-doc update'
end

desc 'Compile *.adoc files to *.html'
task :compile do
  sh 'bundle exec nanoc'
end

desc 'Run server'
task :server do
  sh 'bundle exec nanoc view --host 0.0.0.0 --port 3001'
end

# 略

config.rb では、以下の設定が行なえます。

  • project_dir: Godot プロジェクトのディレクトリを指定します。デフォルトでは、Godot プロジェクトの直下に gd_doc ディレクトリを作成するのを想定していますが、この値を変更すればどこに設置しても大丈夫です。
  • doc_dir: ドキュメントフォルダを指定します。(※ここは基本的に変更することは無いと思います)
  • ignoring_paths: ドキュメントの生成から除外するディレクトリを指定します。addons をデフォルトで除外してますが、コメントアウトして含めるようにすれば、気になるアドオンの処理も確認できます。
config.rb
GdDoc.configure do |config|
  # = Configure the Godot project directory
  # config.project_dir = '../' # Path to project.godot directory
  config.project_dir = '../'

  # = Configure the documentation directory to compile
  # config.doc_dir = '.' # Current directory
  config.doc_dir = '.'

  # = Configure the paths to ignore during compilation
  # config.ignoring_paths = ['addons', 'test', 'tmp']
  config.ignoring_paths = [
    'addons',
    'test',
    'tmp',
  ]
end

2. $ rake コマンドを実行する

先ほど作成された gd_doc ディレクトリに移動して、$ rake コマンドを実行すると、ドキュメントが生成されて、Web サーバが起動します。

$ cd gd_doc

$ rake

解説

$ rake コマンドを実行すると、以下のような標準出力が出てくると思います。

gd-doc update
write ./content/index.adoc
write ./content/scenes/path/to/scene_name.tscn.adoc

bundle exec nanoc
Loading site… done
Compiling site…
create  [0.01s]  output/path/to/scene_name.tscn/index.html

Site compiled in 3.00s.
bundle exec nanoc view --host 0.0.0.0 --port 3001
View the site at http://0.0.0.0:3001/
Puma starting in single mode...
* Puma version: 7.1.0 ("Neon Witch")
* Ruby version: ruby 3.4.7 (2025-10-08 revision 7a5688e2a2) +PRISM [x86_64-linux]
*  Min threads: 0
*  Max threads: 5
*  Environment: development
*          PID: 165097
* Listening on http://0.0.0.0:3001
Use Ctrl-C to stop

$ rake コマンド内では、以下の処理が順に実行されます。

  1. $ gd-doc update
    1. Godot プロジェクト配下のファイルを収集
    2. Tree-sitter でパース
    3. パースした結果を組み立てて、*.adoc ファイルを生成し、content ディレクトリに設置
  2. $ bundle exec nanoc
    • content ディレクトリの内容を HTML ファイルにコンパイルして、output ディレクトリに出力します。
    • content 内で削除されたファイルがあれば、output からも削除します。
  3. $ bundle exec nanoc view --host 0.0.0.0 --port 3001
    • 3001 番ポートで、Web サーバ(Puma) を起動します。
    • 違うポートで起動したい場合は、Rakefile を書き換えて使ってください。

3. ブラウザでアクセスする

以上でドキュメントは生成されています。

改めて手順をおさらいすると、以下になります。

$ cd your_godot_project
$ gd-doc install
$ cd gd_doc
$ bundle install ※ `gd-doc install` の実行自体が初回の場合のみ。一度入れた後は不要。
$ rake

http://localhost:3001 にアクセスすると、生成されたドキュメントが閲覧できます。

各種設定方法

Godot プロジェクト外のディレクトリからドキュメントを生成する場合

例えば、以下のようなディレクトリ構成で動かしたい場合

.
├── gd_doc
│   └── config.rb
└── godot_project
    └── project.godot

config.rbproject_dir を変更すれば動作します。

config.rb
GdDoc.configure do |config|
  config.project_dir = '../godot_project'

  # 略
end

GitHub Pages にデプロイする場合

GitHub Pages は、docs ディレクトリがあればそこを見て動作可能なので、docs ディレクトリに HTML を出力するようにします。

nanoc.yamloutput_dir を変更すれば、デフォルトの output ディレクトリ以外に出力できます。

nanoc.yaml
output_dir: ../docs   # Godot プロジェクト直下の docs に出力する場合
# もしくは、
output_dir: docs      # gd_doc ディレクトリ直下の docs に出力する場合

Kroki を kroki.io ではなく、自前で用意する場合

デフォルトでは、PlantUML を描画するのに、https://kroki.io にアクセスします。

外部サイトにデータを渡して描画するので、それを避けたい人もいると思います。

自前で用意するには、Kroki の Docker イメージがあるので、それを使うことで、例えば http://localhost:8000 で起動できます。
https://hub.docker.com/r/yuzutech/kroki

Kroki サーバの用意ができたら、Rules ファイルの kroki-server-url を修正することで変更できます。

Rules
compile '/**/*.adoc' do
  filter :asciidoctor, \
    attributes: {
      'kroki-server-url' => 'http://localhost:8000'
      # 'kroki-server-url' => 'https://kroki.io',
      'source-highlighter' => 'rouge',
      'rouge-css' => 'class',
    }

  layout '/default.*'
  filter :relativize_paths, type: :html
  # 略

レイアウトや CSS、JS を変えたい場合

gd_doc ディレクトリ配下のうち、

  • content/css
  • content/js
  • content/index.adoc
  • content/resources
  • content/scenes
  • content/scripts
  • content/assets

以外は、初回の $ gd-doc install した後は、$ rake コマンドで更新しません。

.
├── Gemfile
├── Gemfile.lock
├── Rakefile
├── Rules
├── config.rb
├── content
│   ├── css        ※ `$ rake` で書き換わる
│   ├── js         ※ `$ rake` で書き換わる
│   ├── index.adoc ※ `$ rake` で書き換わる
│   ├── assets     ※ `$ rake` で書き換わる
│   ├── resources  ※ `$ rake` で書き換わる
│   ├── scenes     ※ `$ rake` で書き換わる
│   └── scripts    ※ `$ rake` で書き換わる
├── layouts
│   ├── contents-menu.html
│   ├── default.html
│   ├── footer.html
│   └── header.html
├── lib
│   ├── boot.rb
│   └── helper.rb
├── nanoc.yaml
└── tmp

なので、layouts や 上記以外の content 配下を修正することで、見た目や動きを変えることができます。

技術解説

長々と、使い方メインの記事になってしまいましたが、大部分は Tree-sitter と Nanoc、 Asciidoctor のおかげで動いているので、特に語ることも無いかなというのが正直なところですが。

パース処理に関して少しだけ解説します。

*.tscn ファイルについて

Godot のシーンファイルである *.tscn は、以下のような形式になっています。INI や TOML によく似た形式のファイルになっています。

player.tscn
[gd_scene load_steps=42 format=3 uid="uid://c8777f6ryw7re"]

[ext_resource type="Script" uid="uid://dgfjtnknt37a2" path="res://src/player/player.gd" id="1_5irfl"]
[ext_resource type="Texture2D" uid="uid://cp4bljrqpo6mr" path="res://assets/images/player/Player_idle.png" id="2_byvol"]
[ext_resource type="PackedScene" uid="uid://dsnowtvsnihd3" path="res://src/player/ghost_effect/ghost_effect.tscn" id="2_xv5vk"]
[ext_resource type="Texture2D" uid="uid://cdjwu0xy56bnb" path="res://assets/images/player/Player_run.png" id="3_htsrw"]
[ext_resource type="Shader" uid="uid://c8ii4dnxptp45" path="res://src/player/player.gdshader" id="4_b13k1"]
[ext_resource type="Texture2D" uid="uid://ci3bufub70pg2" path="res://assets/images/player/Player_jump.png" id="4_gf1li"]
[ext_resource type="Texture2D" uid="uid://dd4nn447lspus" path="res://assets/images/player/Player_fall.png" id="5_vvr2x"]
[ext_resource type="Texture2D" uid="uid://do4etxl4kmat3" path="res://assets/images/player/effects/Burst.png" id="8_l271a"]
[ext_resource type="PackedScene" uid="uid://bb3rohjwfowwf" path="res://src/player/camera/camera.tscn" id="8_x42xx"]

[sub_resource type="ShaderMaterial" id="ShaderMaterial_jm5te"]
shader = ExtResource("4_b13k1")
shader_parameter/dashed = false

# 略

[node name="AnimationTree" type="AnimationTree" parent="."]
unique_name_in_owner = true
root_node = NodePath("%AnimationTree/..")
tree_root = SubResource("AnimationNodeStateMachine_mwkb6")
anim_player = NodePath("../AnimationPlayer")
parameters/Fall/blend_position = 0.195104
parameters/Idle/blend_position = -0.27003
parameters/Jump/blend_position = 0.414688
parameters/Run/blend_position = -0.997033

[connection signal="dashed" from="." to="." method="_on_dashed"]
[connection signal="area_entered" from="FailureCollision" to="." method="_on_failure_collision_area_entered"]
[connection signal="area_entered" from="FrameSubject" to="." method="_on_frame_subject_area_entered"]
[connection signal="area_exited" from="FrameSubject" to="." method="_on_frame_subject_area_exited"]

シーンに関する情報は、上記ファイルにすべて入っています。

なので、このファイルをパースして情報を整理すれば、子ノードの情報や、インスペクタで上書きされた値、シグナルの設定などが取得できます。

Tree-sitter について

Tree-sitter は、Neovim や Helix エディタのシンタックスハイライトのために使われてるという知識しかなかったのですが、Ruby バインディングがあってとても助かりました。
https://github.com/Faveod/ruby-tree-sitter

以下のように使いました。

lib/gd_doc/parser.rb
require 'tree_stand'

module GdDoc
  class Parser
    class << self
      attr_accessor :name, :extensions, :store_raw_data

      def build
        new(files[0])
      end

      def build_all
        files.map{|file| new(file) }
      end

      def parser
        TreeStand::Parser.new(name)
      end

      def files
        targets = extensions.map{|ext| "#{GdDoc.config.project_dir_absolute}/**/*.#{ext}" }
        Dir[*targets].reject{|path|
            next true if File.directory?(path)
            rel_path = relativized_path(path)
            GdDoc.config.ignoring_paths.any?{|str| rel_path.to_s.start_with? str }
          }
      end

      def relativized_path(path)
        Pathname(path).realpath.relative_path_from(GdDoc.config.project_dir_absolute)
      end
    end

    include TreeNodeHelper

    attr_accessor(
      :file,
      :path,
      :raw_data,
    )

    def initialize(file)
      print("Readling file: #{file}...\r") if GdDoc.config.log_verbose
      self.file = file
      self.path = "res://#{GdDoc::Parser.relativized_path(file)}"
      root = self.class.parser.parse_string(File.read(file)).root_node
      self.raw_data = root.text if self.class.store_raw_data
      initializer
      parse(root)
    end

    def initializer
      # Override this method
    end

    def parse(root)
      # Override this method
    end

    def relative_path
      path.delete_prefix 'res://'
    end
  end
end
lib/gd_doc/scene.rb
module GdDoc
  class Scene < Parser
    self.name = 'godot-resource'
    self.extensions = ['tscn']

    # 略
    
    attr_accessor(
      :uid,
      :script_path,
      :script,
      :sections,
      :root_node,
      :child_nodes,
      :connections,
    )

    def initializer
      self.sections = []
      self.child_nodes = []
      self.connections = []
    end

    def parse(root)
      root.children.each do |child|
        case child.type
        when :section
          self.sections << TreeNode::Section.new(child)
        end
      end

      self.uid = value_of('gd_scene', 'uid')
      self.script_path = sections.map(&:script_path).compact[0]
      build_nodes
      build_connections
    end

    # 略
  end
end
lib/gd_doc/script.rb
module GdDoc
  class Script < Parser
    self.name = 'gdscript'
    self.extensions = ['gd']
    self.store_raw_data = true

    attr_accessor(
      :extends,
      :class_name,
      :signals,
      :functions,
      :variables,
      :constants,
    )

    def initializer
      self.signals   = []
      self.functions = []
      self.variables = []
      self.constants = []
    end

    def parse(root)
      root.each do |child|
        case child.type
        when :extends_statement
          self.extends = dig(child, :type, :identifier)&.text
        when :class_name_statement
          self.class_name = dig(child, :name)&.text
        when :signal_statement
          self.signals << TreeNode::Signal.new(child)
        when :function_definition
          self.functions << TreeNode::Function.new(child)
        when :variable_statement
          self.variables << TreeNode::Variable.new(child)
        when :const_statement
          self.constants << TreeNode::Constant.new(child)
        end
      end
    end
  end
end

余談

実は、*.tscn ファイルをパースして、PlantUML でクラス図を自動生成するというアイデア自体は元からあり、過去2回挑戦してました。

1回目 は、私が Godot を触り始めた 2022 年ごろに Godot 3 で Action RPG を作成するチュートリアル動画 (※その動画の GitHub リポジトリはこちら) を写経しながら、あまりにわからなかったのでチュートリアルそっちのけで、正規表現と PEG パーサ でパースして作成したりしてました。

2回目は、Godot 4 に上がった際に、GDScript の仕様が変わって前のやつが動かなくなったのと、そもそも良い出来ではなかったので、Julia の PEG パーサDocumenter.jl で作ろうとしてました。

いずれも満足する出来ではなく、パフォーマンスが悪かったり、開発し続ける気力と実力もなく、結局手で PlantUML を描いてました。

特に大きかったのは、*.tscn ファイルだけだったら、まだ正規表現と PEG パーサで対応できそうな感じなんですが、GDScript でシグナルを紐づけているところを図示しようと思うと、ちゃんと GDScript を字句解析・構文解析してパースする必要があったからです。

今回、Tree-sitter を使いましたが、その辺のめんどくさいところはやらなくて済み、パースされた結果を組み立てて AsciiDoc に落とし込むだけなのが良かったです。
(※と言っても、GDScript 側でシグナルを紐づけている箇所の図示はまだできていません。今後対応したい。)

この形なら今後もメンテ出来そうだなと。

やっぱり、パーサは自分で書くものじゃないなと。改めて思いました。

今後について

とりあえず動くようになったという状態に近いので、
いろいろとやりたいことが、まだまだあります。

  • GDScript 側でシグナルを紐づけている箇所の図示
  • 検索機能の追加(Fuse.js とか入れればいいのかな?)
  • AnimationPlayer の内容をシーケンス図で図示
  • Collision Layer に関する情報の出力
  • スタッツの表示
  • その他いろいろ

ちょっとずつ加えていきたいと思います。

最後に

ここまで書いといてなんですが。
たぶん、これを使おうとする奇特な方はほとんどいないかなと。
自分がそうなので。

手順的に

  • Ruby のインストール
  • tree-sitter のインストール
  • git clone
  • ln -s で gd-doc コマンドのシンボリックリンク追加

まで行くところが、めんどくさそうだし、いろいろ入れないといけないのが躊躇するかなと。

$ gd-doc コマンドが使えるようになるまでできたら、後は Rakefile とか dry-cli で簡単に使えるようにできた自信はあるので、興味を持った方がいれば試してもらえると幸いです。

gd-doc 自体の更新を取り込むのも、わざわざインストールしたディレクトリに移動して $ git pull しなくても、$ gd-doc upgrade コマンドを用意しています。

$ gd-doc upgrade コマンドの中身
lib/gd_doc/commands/upgrade.rb
module GdDoc::Commands
  class Upgrade < Dry::CLI::Command
    desc 'Upgrade gd-doc'

    def call(*)
      cd GdDoc::ROOT_DIR do
        sh 'git pull'
        sh 'git submodule update --init --recursive'
        sh 'bundle install'
        sh 'rake build'
      end
    end
  end
end

特に Godot 初心者の方で、自分が Godot を触り始めた 2022 年頃と同じような疑問を抱いた方がいたら、GitHub で公開されている Godot 製のプロジェクトとかに対して $ gd-doc install コマンドでドキュメントを生成して勉強の役に立ててもらえたらうれしいなと。

何か要望とか不具合あれば、コメントで教えてください。

以上です。

Discussion