godot

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Godot Skill

Godot 实战技能

Develop, test, build, and deploy Godot 4.x games.
开发、测试、构建和部署Godot 4.x游戏。

Quick Reference

快速参考

bash
undefined
bash
undefined

GdUnit4 - Unit testing framework (GDScript, runs inside Godot)

GdUnit4 - Unit testing framework (GDScript, runs inside Godot)

godot --headless --path . -s res://addons/gdUnit4/bin/GdUnitCmdTool.gd --run-tests
godot --headless --path . -s res://addons/gdUnit4/bin/GdUnitCmdTool.gd --run-tests

PlayGodot - Game automation framework (Python, like Playwright for games)

PlayGodot - Game automation framework (Python, like Playwright for games)

export GODOT_PATH=/path/to/godot-automation-fork pytest tests/ -v
export GODOT_PATH=/path/to/godot-automation-fork pytest tests/ -v

Export web build

Export web build

godot --headless --export-release "Web" ./build/index.html
godot --headless --export-release "Web" ./build/index.html

Deploy to Vercel

Deploy to Vercel

vercel deploy ./build --prod

---
vercel deploy ./build --prod

---

Testing Overview

测试概述

GdUnit4PlayGodot
TypeUnit testingGame automation
LanguageGDScriptPython
RunsInside GodotExternal (like Playwright)
RequiresAddonCustom Godot fork
Best forUnit/component testsE2E/integration tests

GdUnit4PlayGodot
类型单元测试游戏自动化测试
语言GDScriptPython
运行环境Godot内部外部环境(类似Playwright)
依赖插件定制版Godot分支
最佳适用场景单元/组件测试端到端/集成测试

GdUnit4 (GDScript Tests)

GdUnit4(GDScript测试)

GdUnit4 runs tests written in GDScript directly inside Godot.
GdUnit4 可直接在Godot内部运行用GDScript编写的测试。

Project Structure

项目结构

project/
├── addons/gdUnit4/          # GdUnit4 addon
├── test/                    # Test directory
│   ├── game_test.gd
│   └── player_test.gd
└── scripts/
    └── game.gd
project/
├── addons/gdUnit4/          # GdUnit4 addon
├── test/                    # Test directory
│   ├── game_test.gd
│   └── player_test.gd
└── scripts/
    └── game.gd

Setup

安装设置

bash
undefined
bash
undefined

Install GdUnit4

Install GdUnit4

git clone --depth 1 https://github.com/MikeSchulze/gdUnit4.git addons/gdUnit4
git clone --depth 1 https://github.com/MikeSchulze/gdUnit4.git addons/gdUnit4

Enable plugin in Project Settings → Plugins

Enable plugin in Project Settings → Plugins

undefined
undefined

Basic Unit Test

基础单元测试

gdscript
undefined
gdscript
undefined

test/game_test.gd

test/game_test.gd

extends GdUnitTestSuite
var game: Node
func before_test() -> void: game = auto_free(load("res://scripts/game.gd").new())
func test_initial_state() -> void: assert_that(game.is_game_active()).is_true() assert_that(game.get_current_player()).is_equal("X")
func test_make_move() -> void: var success := game.make_move(4) assert_that(success).is_true() assert_that(game.get_board_state()[4]).is_equal("X")
undefined
extends GdUnitTestSuite
var game: Node
func before_test() -> void: game = auto_free(load("res://scripts/game.gd").new())
func test_initial_state() -> void: assert_that(game.is_game_active()).is_true() assert_that(game.get_current_player()).is_equal("X")
func test_make_move() -> void: var success := game.make_move(4) assert_that(success).is_true() assert_that(game.get_board_state()[4]).is_equal("X")
undefined

Scene Test with Input Simulation

带输入模拟的场景测试

gdscript
undefined
gdscript
undefined

test/game_scene_test.gd

test/game_scene_test.gd

extends GdUnitTestSuite
var runner: GdUnitSceneRunner
func before_test() -> void: runner = scene_runner("res://scenes/main.tscn")
func after_test() -> void: runner.free()
func test_click_cell() -> void: await runner.await_idle_frame()
var cell = runner.find_child("Cell4")
runner.set_mouse_position(cell.global_position + cell.size / 2)
runner.simulate_mouse_button_pressed(MOUSE_BUTTON_LEFT)
await runner.await_input_processed()

var game = runner.scene()
assert_that(game.get_board_state()[4]).is_equal("X")
func test_keyboard_restart() -> void: runner.simulate_key_pressed(KEY_R) await runner.await_input_processed() assert_that(runner.scene().is_game_active()).is_true()
undefined
extends GdUnitTestSuite
var runner: GdUnitSceneRunner
func before_test() -> void: runner = scene_runner("res://scenes/main.tscn")
func after_test() -> void: runner.free()
func test_click_cell() -> void: await runner.await_idle_frame()
var cell = runner.find_child("Cell4")
runner.set_mouse_position(cell.global_position + cell.size / 2)
runner.simulate_mouse_button_pressed(MOUSE_BUTTON_LEFT)
await runner.await_input_processed()

var game = runner.scene()
assert_that(game.get_board_state()[4]).is_equal("X")
func test_keyboard_restart() -> void: runner.simulate_key_pressed(KEY_R) await runner.await_input_processed() assert_that(runner.scene().is_game_active()).is_true()
undefined

Running GdUnit4 Tests

运行GdUnit4测试

bash
undefined
bash
undefined

All tests

All tests

godot --headless --path . -s res://addons/gdUnit4/bin/GdUnitCmdTool.gd --run-tests
godot --headless --path . -s res://addons/gdUnit4/bin/GdUnitCmdTool.gd --run-tests

Specific test file

Specific test file

godot --headless --path . -s res://addons/gdUnit4/bin/GdUnitCmdTool.gd
--run-tests --add res://test/my_test.gd
godot --headless --path . -s res://addons/gdUnit4/bin/GdUnitCmdTool.gd
--run-tests --add res://test/my_test.gd

Generate reports for CI

Generate reports for CI

godot --headless --path . -s res://addons/gdUnit4/bin/GdUnitCmdTool.gd
--run-tests --report-directory ./reports
undefined
godot --headless --path . -s res://addons/gdUnit4/bin/GdUnitCmdTool.gd
--run-tests --report-directory ./reports
undefined

GdUnit4 Assertions

GdUnit4 断言方法

gdscript
undefined
gdscript
undefined

Values

Values

assert_that(value).is_equal(expected) assert_that(value).is_not_null() assert_that(condition).is_true()
assert_that(value).is_equal(expected) assert_that(value).is_not_null() assert_that(condition).is_true()

Numbers

Numbers

assert_that(number).is_greater(5) assert_that(number).is_between(1, 100)
assert_that(number).is_greater(5) assert_that(number).is_between(1, 100)

Strings

Strings

assert_that(text).contains("expected") assert_that(text).starts_with("prefix")
assert_that(text).contains("expected") assert_that(text).starts_with("prefix")

Arrays

Arrays

assert_that(array).contains(element) assert_that(array).has_size(5)
assert_that(array).contains(element) assert_that(array).has_size(5)

Signals

Signals

await assert_signal(node).is_emitted("signal_name")
undefined
await assert_signal(node).is_emitted("signal_name")
undefined

Scene Runner Input API

场景运行器输入API

gdscript
undefined
gdscript
undefined

Mouse

Mouse

runner.set_mouse_position(Vector2(100, 100)) runner.simulate_mouse_button_pressed(MOUSE_BUTTON_LEFT) runner.simulate_mouse_button_released(MOUSE_BUTTON_LEFT)
runner.set_mouse_position(Vector2(100, 100)) runner.simulate_mouse_button_pressed(MOUSE_BUTTON_LEFT) runner.simulate_mouse_button_released(MOUSE_BUTTON_LEFT)

Keyboard

Keyboard

runner.simulate_key_pressed(KEY_SPACE) runner.simulate_key_pressed(KEY_S, false, true) # Ctrl+S
runner.simulate_key_pressed(KEY_SPACE) runner.simulate_key_pressed(KEY_S, false, true) # Ctrl+S

Input actions

Input actions

runner.simulate_action_pressed("jump") runner.simulate_action_released("jump")
runner.simulate_action_pressed("jump") runner.simulate_action_released("jump")

Waiting

Waiting

await runner.await_input_processed() await runner.await_idle_frame() await runner.await_signal("game_over", [], 5000)

---
await runner.await_input_processed() await runner.await_idle_frame() await runner.await_signal("game_over", [], 5000)

---

PlayGodot (Game Automation)

PlayGodot(游戏自动化)

PlayGodot is a game automation framework for Godot - like Playwright, but for games. It enables E2E testing, automated gameplay, and external control of Godot games via the native RemoteDebugger protocol.
Requirements:
PlayGodot 是针对Godot的游戏自动化框架——类似游戏领域的Playwright。它支持端到端测试、自动化游戏玩法,并通过原生RemoteDebugger协议实现对Godot游戏的外部控制。
依赖要求:

Setup

安装设置

bash
undefined
bash
undefined

Install PlayGodot

Install PlayGodot

python -m venv .venv source .venv/bin/activate # Windows: .venv\Scripts\activate pip install playgodot
python -m venv .venv source .venv/bin/activate # Windows: .venv\Scripts\activate pip install playgodot

Option 1: Download pre-built binary (recommended)

Option 1: Download pre-built binary (recommended)

- godot-automation-linux-x86_64.zip

- godot-automation-linux-x86_64.zip

- godot-automation-macos-universal.zip (Intel + Apple Silicon)

- godot-automation-macos-universal.zip (Intel + Apple Silicon)

Option 2: Build custom Godot fork from source

Option 2: Build custom Godot fork from source

git clone https://github.com/Randroids-Dojo/godot.git cd godot && git checkout automation scons platform=macos arch=arm64 target=editor -j8 # macOS Apple Silicon
git clone https://github.com/Randroids-Dojo/godot.git cd godot && git checkout automation scons platform=macos arch=arm64 target=editor -j8 # macOS Apple Silicon

scons platform=macos arch=x86_64 target=editor -j8 # macOS Intel

scons platform=macos arch=x86_64 target=editor -j8 # macOS Intel

scons platform=linuxbsd target=editor -j8 # Linux

scons platform=linuxbsd target=editor -j8 # Linux

scons platform=windows target=editor -j8 # Windows

scons platform=windows target=editor -j8 # Windows

undefined
undefined

Test Configuration (conftest.py)

测试配置(conftest.py)

python
import os
import pytest_asyncio
from pathlib import Path
from playgodot import Godot

GODOT_PROJECT = Path(__file__).parent.parent
GODOT_PATH = os.environ.get("GODOT_PATH", "/path/to/godot-fork")

@pytest_asyncio.fixture
async def game():
    async with Godot.launch(
        str(GODOT_PROJECT),
        headless=True,
        timeout=15.0,
        godot_path=GODOT_PATH,
    ) as g:
        await g.wait_for_node("/root/Game")
        yield g
python
import os
import pytest_asyncio
from pathlib import Path
from playgodot import Godot

GODOT_PROJECT = Path(__file__).parent.parent
GODOT_PATH = os.environ.get("GODOT_PATH", "/path/to/godot-fork")

@pytest_asyncio.fixture
async def game():
    async with Godot.launch(
        str(GODOT_PROJECT),
        headless=True,
        timeout=15.0,
        godot_path=GODOT_PATH,
    ) as g:
        await g.wait_for_node("/root/Game")
        yield g

Writing PlayGodot Tests

编写PlayGodot测试

python
import pytest

GAME = "/root/Game"

@pytest.mark.asyncio
async def test_game_starts_empty(game):
    board = await game.call(GAME, "get_board_state")
    assert board == ["", "", "", "", "", "", "", "", ""]

@pytest.mark.asyncio
async def test_clicking_cell(game):
    await game.click("/root/Game/VBoxContainer/GameBoard/GridContainer/Cell4")
    board = await game.call(GAME, "get_board_state")
    assert board[4] == "X"

@pytest.mark.asyncio
async def test_game_win(game):
    for pos in [0, 3, 1, 4, 2]:  # X wins top row
        await game.call(GAME, "make_move", [pos])

    is_active = await game.call(GAME, "is_game_active")
    assert is_active is False
python
import pytest

GAME = "/root/Game"

@pytest.mark.asyncio
async def test_game_starts_empty(game):
    board = await game.call(GAME, "get_board_state")
    assert board == ["", "", "", "", "", "", "", "", ""]

@pytest.mark.asyncio
async def test_clicking_cell(game):
    await game.click("/root/Game/VBoxContainer/GameBoard/GridContainer/Cell4")
    board = await game.call(GAME, "get_board_state")
    assert board[4] == "X"

@pytest.mark.asyncio
async def test_game_win(game):
    for pos in [0, 3, 1, 4, 2]:  # X wins top row
        await game.call(GAME, "make_move", [pos])

    is_active = await game.call(GAME, "is_game_active")
    assert is_active is False

Running PlayGodot Tests

运行PlayGodot测试

bash
export GODOT_PATH=/path/to/godot-automation-fork
pytest tests/ -v
pytest tests/test_game.py::test_clicking_cell -v
bash
export GODOT_PATH=/path/to/godot-automation-fork
pytest tests/ -v
pytest tests/test_game.py::test_clicking_cell -v

PlayGodot API

PlayGodot API

python
undefined
python
undefined

Node interaction

Node interaction

node = await game.get_node("/root/Game") await game.wait_for_node("/root/Game", timeout=10.0) exists = await game.node_exists("/root/Game") result = await game.call("/root/Node", "method", [arg1, arg2]) value = await game.get_property("/root/Node", "property") await game.set_property("/root/Node", "property", value)
node = await game.get_node("/root/Game") await game.wait_for_node("/root/Game", timeout=10.0) exists = await game.node_exists("/root/Game") result = await game.call("/root/Node", "method", [arg1, arg2]) value = await game.get_property("/root/Node", "property") await game.set_property("/root/Node", "property", value)

Node queries

Node queries

paths = await game.query_nodes("Button") count = await game.count_nodes("Label")
paths = await game.query_nodes("Button") count = await game.count_nodes("Label")

Mouse input

Mouse input

await game.click("/root/Button") await game.click(300, 200) await game.double_click("/root/Button") await game.right_click(100, 100) await game.drag("/root/Item", "/root/Slot")
await game.click("/root/Button") await game.click(300, 200) await game.double_click("/root/Button") await game.right_click(100, 100) await game.drag("/root/Item", "/root/Slot")

Keyboard input

Keyboard input

await game.press_key("space") await game.press_key("ctrl+s") await game.type_text("hello")
await game.press_key("space") await game.press_key("ctrl+s") await game.type_text("hello")

Input actions

Input actions

await game.press_action("jump") await game.hold_action("sprint", 2.0)
await game.press_action("jump") await game.hold_action("sprint", 2.0)

Touch input

Touch input

await game.tap(300, 200) await game.swipe(100, 100, 400, 100) await game.pinch((200, 200), 0.5)
await game.tap(300, 200) await game.swipe(100, 100, 400, 100) await game.pinch((200, 200), 0.5)

Screenshots

Screenshots

png_bytes = await game.screenshot() await game.screenshot("/tmp/screenshot.png") similarity = await game.compare_screenshot("expected.png") await game.assert_screenshot("reference.png", threshold=0.99)
png_bytes = await game.screenshot() await game.screenshot("/tmp/screenshot.png") similarity = await game.compare_screenshot("expected.png") await game.assert_screenshot("reference.png", threshold=0.99)

Scene management

Scene management

scene = await game.get_current_scene() await game.change_scene("res://scenes/level2.tscn") await game.reload_scene()
scene = await game.get_current_scene() await game.change_scene("res://scenes/level2.tscn") await game.reload_scene()

Game state

Game state

await game.pause() await game.unpause() is_paused = await game.is_paused() await game.set_time_scale(0.5) scale = await game.get_time_scale()
await game.pause() await game.unpause() is_paused = await game.is_paused() await game.set_time_scale(0.5) scale = await game.get_time_scale()

Waiting

Waiting

await game.wait_for_node("/root/Game/SpawnedEnemy", timeout=5.0) await game.wait_for_visible("/root/Game/UI/GameOverPanel", timeout=10.0) await game.wait_for_signal("game_over") await game.wait_for_signal("health_changed", source="/root/Game/Player")

---
await game.wait_for_node("/root/Game/SpawnedEnemy", timeout=5.0) await game.wait_for_visible("/root/Game/UI/GameOverPanel", timeout=10.0) await game.wait_for_signal("game_over") await game.wait_for_signal("health_changed", source="/root/Game/Player")

---

Building & Deployment

构建与部署

Web Export

网页导出

bash
undefined
bash
undefined

Requires export_presets.cfg with Web preset

Requires export_presets.cfg with Web preset

godot --headless --export-release "Web" ./build/index.html
undefined
godot --headless --export-release "Web" ./build/index.html
undefined

Export Preset (export_presets.cfg)

导出预设(export_presets.cfg)

ini
[preset.0]
name="Web"
platform="Web"
runnable=true
export_path="build/index.html"
ini
[preset.0]
name="Web"
platform="Web"
runnable=true
export_path="build/index.html"

Deploy to Vercel

部署到Vercel

bash
npm i -g vercel
vercel deploy ./build --prod

bash
npm i -g vercel
vercel deploy ./build --prod

CI/CD

CI/CD

GitHub Actions Example

GitHub Actions 示例

yaml
- name: Setup Godot
  uses: chickensoft-games/setup-godot@v2
  with:
    version: 4.3.0
    include-templates: true

- name: Run GdUnit4 Tests
  run: |
    godot --headless --path . \
      -s res://addons/gdUnit4/bin/GdUnitCmdTool.gd \
      --run-tests --report-directory ./reports

- name: Upload Results
  uses: actions/upload-artifact@v4
  if: always()
  with:
    name: test-results
    path: reports/

yaml
- name: Setup Godot
  uses: chickensoft-games/setup-godot@v2
  with:
    version: 4.3.0
    include-templates: true

- name: Run GdUnit4 Tests
  run: |
    godot --headless --path . \
      -s res://addons/gdUnit4/bin/GdUnitCmdTool.gd \
      --run-tests --report-directory ./reports

- name: Upload Results
  uses: actions/upload-artifact@v4
  if: always()
  with:
    name: test-results
    path: reports/

References

参考资料

  • references/gdunit4-quickstart.md
    - GdUnit4 setup
  • references/scene-runner.md
    - Input simulation API
  • references/assertions.md
    - Assertion methods
  • references/playgodot.md
    - PlayGodot guide
  • references/deployment.md
    - Deployment guide
  • references/ci-integration.md
    - CI/CD setup
  • references/gdunit4-quickstart.md
    - GdUnit4快速入门
  • references/scene-runner.md
    - 场景运行器说明
  • references/assertions.md
    - 断言方法文档
  • references/playgodot.md
    - PlayGodot指南
  • references/deployment.md
    - 部署指南
  • references/ci-integration.md
    - CI/CD集成说明