Porting Games from Godot¶
This page describes part of the Python framework that comes in the
acron wheel. It has no C++ counterpart.
Much of the acron package grew out of porting finished Godot games – a
third-person shooter, a racing game, a 2D tile platformer – to Acron. What
those ports had in common was kept: the movers, the fixed-step loop, the
coordinate conventions, and readers for Godot’s scene and resource files.
This page is about the parts that exist for porting; the mechanisms
themselves are described with the rest of the framework (see
Physics and Characters, The Game Loop and
2D Games).
How close ported code stays¶
A port stays close to its source at the statement level, not the framework level. Ported game code can read like the GDScript it came from, so it can be checked line by line against the original:
def _physics_process(self, dt):
if self.is_on_floor() and self.velocity.z < 0.0:
self.velocity.z = 0.0
self.velocity += self.get_gravity() * dt
if self.input.is_action_just_pressed("jump") and self.is_on_floor():
self.velocity.z = JUMP_SPEED
self.move_and_slide(dt)
The vocabulary the game’s own scripts use is kept: move_and_slide,
is_on_floor (CharacterBody3D and
CharacterBody2D), is_action_pressed and
is_action_just_pressed (InputMap),
Signal.connect and emit (Signal), and the
_physics_process(dt) hook, which GameLoop
calls at a fixed 60 Hz for every object passed to loop.register.
Godot’s machinery is not reproduced: there is no scene tree lifecycle, no
_ready or _enter_tree, no autoloads, no inspector or @export, no
NodePath string addressing. The engine’s scene graph is the scene tree;
parenting, transforms, rendering, model loading and vector math are done with
the engine directly. A wrapper exists only where it centralises a convention
(the axis change, the collision-layer bits) or adds something the engine
lacks (the character movers, the fixed step, the spring arm).
Y-up and Z-up¶
Godot is Y-up, the engine is Z-up; both are right-handed. glTF models are
converted to Z-up when they are loaded, so a loaded model must not be
rotated again. What still needs converting is every number copied out of a
script or a .tscn: spawn points, Vector3.UP, gravity, hand-typed
directions. acron.interop holds the conventions:
from acron.interop import UP, GRAVITY, gravity_vec, g2p, p2g
spawn = g2p((4.0, 1.5, -2.0)) # Godot (x, y, z) -> (x, z, y)
gravity = gravity_vec(20.0) # a project's custom default_gravity
g2p/p2g (and godot_to_acron_vec/acron_to_godot_vec) swap Y
and Z. The swap is a reflection: it is right for vectors and positions,
never as a rotation basis. For full transforms use
acron.gdformat.transform, below.
Dense ported maths – vehicle dynamics, say – can instead run unchanged in a
Y-up frame: attach_logic_frame(parent) returns a child node rotated so its
local +Y is the parent’s +Z, and g2p_rot/p2g_rot are the matching
proper rotation, (x, y, z) -> (x, -z, y). World data used alongside such a
frame (track collision, markers) must go through g2p_rot, not g2p:
mixing the two mirrors the world against the logic frame.
apply_vertical_fov(lens, degrees) sets a lens from the vertical field of
view that Godot cameras are authored with; Lens.set_fov takes the
horizontal one. Calling it every frame keeps it right after a window resize.
acron.mathutil has the few global-scope helpers ported scripts call by
name, such as move_toward(value, target, delta) (a negative delta
moves away, as in Godot).
Reading Godot files¶
acron.gdformat reads Godot’s text formats – .tscn and .tres,
project.godot, res:// and uid:// paths, the TileMap tile_data
encoding – into the plain data the runtime modules use. It ships in the
wheel so that ported games can keep loading their Godot assets, and is
scheduled to be replaced by Acron’s own scene format; new code should not grow
a dependency on it. Nothing is imported eagerly: import the submodule you
need.
Module |
Reads |
|---|---|
|
|
|
the section parser ( |
|
|
|
|
|
Godot’s importer name suffixes ( |
|
|
|
|
|
|
|
|
|
|
|
|
A 3D level’s collision, for instance, is the primitive shapes authored under
its StaticBody3D nodes, built as Bullet shapes rather than triangle
meshes:
from direct.showbase.ShowBase import ShowBase
from acron.gdformat.res import set_default_root
from acron.gdformat.tscn import build_static_body
from acron.world import PhysicsWorld, layer_bit
set_default_root("path/to/godot_project")
base = ShowBase()
physics = PhysicsWorld()
level = build_static_body("res://level/scenes/structure.tscn")
base.render.attach_new_node(level).set_collide_mask(layer_bit(1))
physics.attach(level)
and the project’s key bindings become an input map:
from acron.gdformat.project import parse_project_actions
from acron.input_map import InputMap
actions = parse_project_actions("path/to/godot_project/project.godot")
inputs = InputMap(base, held=actions, edges={"jump": actions["jump"]})
parse_project_actions maps keyboard events only; joypad bindings are
skipped.
3D particles¶
acron.particles3d replays Godot CPUParticles3D emitters from their
numbers. Burst is a one-shot emitter (fired by an animation track or
a line of script; it removes itself when its particles die) and Loop
re-emits for as long as it exists. Both take the emitter’s parameters as
keyword arguments – geom, amount, lifetime, direction,
spread, velocity, gravity, scale and scale_curve,
color and color_ramp, the emission shape, and more – with directions
in the engine’s frame (godot_dir(x, y, z) converts one). card_geom,
sphere_geom, cone_geom, ray_geom and glb_geom make the
particle geometry.
from acron.particles3d import Burst, Curve, card_geom, godot_dir
Burst(base, base.render, geom=card_geom(0.2, 0.2), amount=24,
lifetime=0.6, pos=(0, 5, 1), direction=godot_dir(0, 1, 0),
spread=45.0, velocity=(2.0, 4.0), gravity=(0, 0, -9.8),
scale_curve=Curve((0.0, 1.0), (1.0, 0.0)),
billboard=True, additive=True)
Each particle is a node integrated in Python on a render task: enough for
effect-sized emitters, tens to low hundreds of particles, not for thousands.
clear_all() destroys every live emitter, for a scene teardown.