Scripting Tutorial
This tutorial walks through writing Python scripts in AxisPy — from attaching a script to an entity up to advanced patterns like coroutines, tweens and events.
Overview
Scripts in AxisPy are plain Python classes attached to an entity via a ScriptComponent. The engine automatically injects useful attributes and helper methods into every script instance so you rarely need to import anything beyond what you actively use.
Automatically injected attributes
self.entity— theEntitythis script is attached to.self.logger— aloggerinstance namedscript.<ClassName>.
Automatically injected helper methods (forwarded from ScriptComponent)
Navigation:
find,get_childrenLifecycle:
destroy,hide,show,process_physics,change_sceneGroups:
call_groupPrefabs:
instantiate_prefab/spawn_prefabCoroutines:
start_coroutine,stop_coroutinesTweens:
tween,cancel_tweensEvents:
subscribe_to_event,unsubscribe_from_event,emit_global_event,emit_local_event,emit_global_event_immediate,emit_local_event_immediate
Editor workflow (no code)
Attach a script to an entity (Inspector) - Select an entity in the Hierarchy. - Inspector → Add Component → Script Component. - In the Script section:
Click the pencil button to open the current script in the Scripts Editor.
Click the folder button → choose “Select Existing Script” to pick a
.pyfile, or “Create New Script” to generate a template file under your project (the class name is auto‑filled).The “Class Name” label reflects the top‑level class the engine will instantiate.
Open and edit scripts (Scripts Editor tab) - Asset Manager: select a
.pyfile → right‑click → “Edit Script”; or use the Inspector’s pencil button. - Save in the Scripts Editor; the runtime hot‑reloads your script automatically. - Shortcuts: Ctrl+F/H (find/replace), Ctrl+G (go to line), Ctrl+/ (toggle comment), Ctrl+D (duplicate line). - The editor provides autocompletion for common engine APIs (Input, components, and injectedself.*helpers).See logs while iterating - Use
self.logger.info/debug/warning/errorin your script. - View output in the Console dock.
Your First Script
Create a file, e.g. my_scripts/hello.py, and define a class:
class Hello:
def on_start(self):
self.logger.info("Hello from", entity=self.entity.name)
def on_update(self, dt: float):
pass
Then attach it to an entity in the editor by setting the ScriptComponent’s Script Path to my_scripts/hello.py and Class Name to Hello.
Lifecycle Methods
The ScriptSystem calls the following methods when they exist on the class.
Method |
When it is called |
|---|---|
|
Once, the first frame the script is active. |
|
Every frame. |
|
Called when the entity becomes visible (via |
|
Called when the entity is hidden (via |
Reading Input
Import Input to poll keyboard and mouse state, or InputMap for named rebindable actions.
import pygame
from core.input import Input
class PlayerController:
SPEED = 200 # pixels per second
def on_update(self, dt: float):
from core.components import Transform
transform = self.entity.get_component(Transform)
if transform is None:
return
if Input.get_key(pygame.K_RIGHT):
transform.x += self.SPEED * dt
if Input.get_key(pygame.K_LEFT):
transform.x -= self.SPEED * dt
if Input.get_key(pygame.K_UP):
transform.y -= self.SPEED * dt
if Input.get_key(pygame.K_DOWN):
transform.y += self.SPEED * dt
Note
Input.get_key(keycode) returns True every frame the key is held.
For named actions (rebindable), prefer InputMap.
For named actions, use InputMap:
import pygame
from core.input_map import InputMap
InputMap.register("jump", [pygame.K_SPACE])
class Jumper:
def on_update(self, dt: float):
if InputMap.is_just_pressed("jump"):
pass # single-frame trigger
if InputMap.is_pressed("jump"):
pass # held every frame
if InputMap.is_just_released("jump"):
pass # release detection
For analogue movement (keyboard + joystick merged), use Input.get_axis:
import pygame
from core.input import Input
class Mover:
SPEED = 200
def on_update(self, dt: float):
from core.components import Transform
transform = self.entity.get_component(Transform)
if transform:
transform.x += Input.get_axis("Horizontal") * self.SPEED * dt
transform.y -= Input.get_axis("Vertical") * self.SPEED * dt
Working with the Transform
Retrieve a component with get_component():
from core.components import Transform
class Spinner:
ROTATION_SPEED = 90 # degrees per second
def on_update(self, dt: float):
transform = self.entity.get_component(Transform)
if transform:
transform.rotation += self.ROTATION_SPEED * dt
Transform exposes x, y, rotation, scale_x, scale_y as read/write properties.
It also provides convenience methods translate, rotate, and scale.
Finding Other Entities
Use self.find(name) to look up another entity in the same world by name:
class CameraFollow:
def on_update(self, dt: float):
player = self.find("Player")
if player is None:
return
from core.components import Transform
player_t = player.get_component(Transform)
my_t = self.entity.get_component(Transform)
if player_t and my_t:
my_t.x = player_t.x
my_t.y = player_t.y
Scene Management
Call self.change_scene(scene_name) to queue a scene transition at the end of the current frame:
import pygame
from core.input import Input
class MainMenu:
def on_update(self, dt: float):
if Input.get_key(pygame.K_SPACE):
self.change_scene("game_scene")
Coroutines
Coroutines let you write time-based logic as a generator function.
Yield Wait to pause for a number of seconds or WaitFrames to pause for a number of frames.
from core.coroutine_manager import Wait, WaitFrames
class Blinker:
def on_start(self):
self.start_coroutine(self._blink())
def _blink(self):
while True:
self.hide()
yield Wait(0.3)
self.show()
yield Wait(0.3)
Cancel all running coroutines on this script with self.stop_coroutines().
Tweens
Tweens smoothly interpolate a numeric property over time.
from core.tween import ease_out_quad
class SlideIn:
def on_start(self):
self.tween(
self.entity,
"transform.x",
target=400.0,
start=0.0,
duration=1.0,
easing=ease_out_quad,
)
Cancel tweens with self.cancel_tweens() or self.cancel_tweens(entity) to target a specific entity.
Events
The event system allows decoupled communication between scripts.
Subscribing to an event
class ScoreDisplay:
def on_start(self):
self.subscribe_to_event("score_changed", self._on_score_changed)
def _on_score_changed(self, new_score: int):
self.logger.info("Score updated", score=new_score)
Emitting an event
class ScoreManager:
_score = 0
def add_points(self, points: int):
self._score += points
self.emit_global_event("score_changed", self._score)
emit_global_event/emit_local_event— queued, dispatched next frame.emit_global_event_immediate/emit_local_event_immediate— dispatched synchronously (zero-latency).
Group Calls
call_group broadcasts a method call to every entity that belongs to a named group and has a script with that method:
import pygame
from core.input import Input
class GameManager:
def on_update(self, dt: float):
if Input.get_key(pygame.K_p):
self.call_group("enemies", "pause")
Entities are added to a group with add_group():
self.entity.add_group("enemies")
Spawning Prefabs
instantiate_prefab (alias: spawn_prefab) loads a saved entity from a .json prefab file and adds it to the world:
from core.components import Transform
class Spawner:
def on_start(self):
t = self.entity.get_component(Transform)
bullet = self.spawn_prefab(
"prefabs/bullet.json",
x=t.x if t else 0.0,
y=t.y if t else 0.0,
)
The path is resolved relative to the script file, AXISPY_PROJECT_PATH, or the current working directory (whichever exists first).
Logging
The injected self.logger is a structured logger. Pass extra context as keyword arguments:
self.logger.debug("Entity moved", x=transform.x, y=transform.y)
self.logger.info("Scene loaded")
self.logger.warning("Missing component", component="Rigidbody")
self.logger.error("Critical failure", reason=str(e))
Hot Reloading
The ScriptSystem monitors each script file’s modification time.
Saving the file while the game is running in the editor automatically reloads the script and calls on_start again on the next frame — no restart required.