Skip to content

Player.first.position = … reverts within ~1s (physics overrides scripted teleport) #50

Description

@dsrw

Repro

In an MCP eval (or any VM script):

Player.first.position = vec3(0, 1, 15)

The console echoes the new position immediately, but within one screenshot
cycle (~200ms) the player is back near its previous spot. Subsequent
screenshot_from_player shows the original view, not the view from
(0, 1, 15).

Working around the issue: the MCP set_position tool with the player's id
does stick. That goes through unit.transform = Transform.init(pos, yaw)
inside a smooth-move loop that re-asserts the transform every ~33ms.

Why I think it's a bug

vmlib/enu/base_api.nim:

proc `position=`*(self: Unit, position: Vector3) =
  self.position_set(position)

src/controllers/script_controllers/host_bridge.nim:position_set writes
transform_value.origin. The change propagates via Ed → PlayerNode's
transform_value.watch (src/nodes/player_node.nim:179-181) which writes
self.transform = change.item on the KinematicBody. So far so good.

But PlayerNode.process (line 252-254) then runs move_and_slide
applying velocity, gravity, and collision — and writes the post-physics
transform back into the model:

self.velocity = self.move_and_slide(velocity, UP)
self.model.transform = self.transform

So a scripted teleport lives for at most one physics tick before being
overwritten by whatever physics resolves the body to (gravity pulls it
toward the ground; collisions push it sideways). For a player standing
on the ground with no input, the next-frame transform is essentially
the previous position again.

This is surprising because position= is a public, documented setter
that names what it does. Users (and AI clients) reasonably expect a
teleport to stick until they move again.

Suggested fix

A teleport via position= should reset velocity and not be clobbered by
the same-frame physics step. A few options, roughly in order of
disruption:

  • Inside position_set, also zero velocity_value when the unit is a
    Player. Smallest change; covers gravity-driven drift but not collision
    resolution if the new position is inside geometry.
  • Skip the model.transform = self.transform write-back in
    PlayerNode.process for one frame after a transform_value change
    came in from the model side (i.e. a teleport).
  • Or document the limitation and add a Player.first.teleport_to(pos)
    that handles both above.

Reproduced on main (commit 01c4041) with the MCP server.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions