Skip to content

02.Coding the player

la.panon. edited this page Apr 18, 2025 · 3 revisions

Please proceed with the Godot Docs - Creating the player scene content first. We will resume the tutorial with the following project configuration:

.
├── art
├── fonts
├── icon.svg
├── icon.svg.import
├── nim
├── player.tscn [New]
└── project.godot

Let's begin this section by defining our Player class.

# nim/nimmain/src/classes/gdplayer.nim
import gdext
import gdext/classes/gdArea2D

type Player* {.gdsync.} = ptr object of Area2D
  speed* {.gdexport.}: float32 = 400
  screen_size: Vector2

Two unique gdext-nim pragmas appeared.

{.gdsync.} is a pragma that allows Godot to recognize classes defined in Nim. When defining a new class, please add an asterisk * and {.gdsync.} to the type name.

{.gdexport.} is a pragma to expose properties to the engine (GDScript). By appending an asterisk * and {.gdexport.} to a property, this property will appear in the inspector and can be referenced from GDScript.

Import this module from bootstrap.nim and build the extension.

# nim/nimmain/bootstrap.nim
  import gdext
+ import classes/gdPlayer


  GDExtensionEntryPoint
gdextwiz build

Now we can replace the Player(Area2D) node in the player scene with our Player type. You should see the Speed property in the inspector.

Replace player node's type with Player Inspector view of Player node that contains Speed property

In gdext-nim, Godot virtual functions override the following:

method ready(self: Player) {.gdsync.} =
  self.screen_size = self.getViewportRect().size

Nim does not allow leading underscores. Instead, replace proc with method. That is the marker for overriding a virtual function. And {.gdsync.} appears here as well. As you might have guessed, if you forget to add {.gdsync.}, Godot will not call that method. Please be careful.

Follow the Godot Docs to complete the input map setup.

Here is the function that handles the input map. Hmm, which is almost identical to that of GDScript.

method process(self: Player; delta: float64) {.gdsync.} =
  var velocity: Vector2
  if Input.isActionPressed "move_right":
    velocity.x += 1
  if Input.isActionPressed "move_left":
    velocity.x -= 1
  if Input.isActionPressed "move_down":
    velocity.y += 1
  if Input.isActionPressed "move_up":
    velocity.y -= 1

  if velocity.length > 0:
    velocity = velocity.normalized * self.speed
    play self.AnimatedSprite2D
  else:
    stop self.AnimatedSprite2D

The main thing to notice is the acquisition of child nodes, which in GDScript is done using the $ operator, which is not supported in gdext-nim. Instead, we do the following

type Player* {.gdsync.} = ptr object of Area2D
  speed* {.gdexport.}: float32 = 400
  screen_size: Vector2
  AnimatedSprite2D: AnimatedSprite2D
  CollisionShape2D: CollisionShape2D

method ready(self: Player) {.gdsync.} =
  self.screen_size = self.getViewportRect().size
  self.AnimatedSprite2D = self/"AnimatedSprite2D" as AnimatedSprite2D
  self.CollisionShape2D = self/"CollisionShape2D" as CollisionShape2D

In other words, it gets the child node once at initialization, casts it, and caches it. The / is an alias for get_node, which retrieves the node whose name is specified by the path. The following complex description also works

discard self/"one/two"/"three"

The / operator returns just a Node, so cast it to the type you want with the as operator. Avoid using the standard Nim type conversion mechanism, which succeeds for upcast but almost always fails for downcast.

Oops, the compiler gets angry cuz the play() and stop() functions are undefined. Let's import the AnimationSprite2D functions.

import gdext/classes/gdAnimationSprite2D

Did you notice that the module names is in camelCase? This is not Nim's style, actually these module names are defined in lowercase. However, the Nim compiler normalizes module names so gdext-nim recommends to use camelCase in import sentence for readability.

Please proceed through the tutorial up to Preparing for collisions, taking into account what has been said so far.

In gdext-nim, signals are defined as follows

proc hit(self: Player): Error {.gdsync, signal.}

You're all set for {.gdsync.}; put it on the functions you want Godot to know about anyway. To define a signal, pass in another {.signal.} pragma and set the return type to Error.

Now let's connect a callback to the signal.

method ready(self: Player) {.gdsync.} =
  self.screen_size = self.getViewportRect().size
  self.AnimatedSprite2D = self/"AnimatedSprite2D" as AnimatedSprite2D
  self.CollisionShape2D = self/"CollisionShape2D" as CollisionShape2D
  discard self.connect("body_entered", self.callable"_on_body_entered")
  hide self

proc onBodyEntered(self: Player; body: Variant) {.gdsync, name: "_on_body_entered".} =
  discard

In this example, the onBodyEntered function is recognized by the Engine by {.gdsync.}, the alias is set by {.name.}, and the connection is made by the connect function.

If you set an alias, the engine will recognize, in this example, the onBodyEntered function as “_on_body_entered”.

To refer to the function via the engine, such as to refer to it from a GDScript or to retrieve a Callable, you should use the case-sensitive name you set whatever {.name.} pragma is set or not.

Okay, let's implement onBodyEntered.

proc onBodyEntered(self: Player; body: Variant) {.gdsync, name: "_on_body_entered".} =
  hide self
  discard self.hit()
  self.CollisionShape2D.setDeferred("disabled", variant true)

To emit a signal, simply call the signal as a function. Of course, you can also use Signal.emit or Object.emitSignal.

discard self.emitSignal("hit")
discard self.signal("hit").emit()

Here is a full example of gdplayer.nim.

import gdext
import gdext/classes/gdArea2D
import gdext/classes/gdInput
import gdext/classes/gdSceneTree
import gdext/classes/gdAnimatedSprite2D
import gdext/classes/gdCollisionShape2D


type Player* {.gdsync.} = ptr object of Area2D
  speed* {.gdexport.}: float32 = 400
  screen_size: Vector2
  AnimatedSprite2D: AnimatedSprite2D
  CollisionShape2D: CollisionShape2D

proc hit(self: Player): Error {.gdsync, signal.}

proc start*(self: Player; pos: Vector2) =
  self.position = pos
  show self
  self.CollisionShape2D.disabled = false

method ready(self: Player) {.gdsync.} =
  self.screen_size = self.getViewportRect().size
  self.AnimatedSprite2D = self/"AnimatedSprite2D" as AnimatedSprite2D
  self.CollisionShape2D = self/"CollisionShape2D" as CollisionShape2D
  discard self.connect("body_entered", self.callable"_on_body_entered")
  hide self

method process(self: Player; delta: float64) {.gdsync.} =
  var velocity: Vector2
  if Input.isActionPressed "move_right":
    velocity.x += 1
  if Input.isActionPressed "move_left":
    velocity.x -= 1
  if Input.isActionPressed "move_down":
    velocity.y += 1
  if Input.isActionPressed "move_up":
    velocity.y -= 1

  if velocity.length > 0:
    velocity = velocity.normalized * self.speed
    play self.AnimatedSprite2D
  else:
    stop self.AnimatedSprite2D

  self.position = self.position + velocity * float32 delta
  self.position = self.position.clamp(Vector2.Zero, self.screen_size)

  if velocity.x != 0:
    self.AnimatedSprite2D.animation = "walk"
    self.AnimatedSprite2D.flip_v = false
    self.AnimatedSprite2D.flip_h = velocity.x < 0
  elif velocity.y != 0:
    self.AnimatedSprite2D.animation = "up"
    self.AnimatedSprite2D.flip_v = velocity.y > 0

proc onBodyEntered(self: Player; body: Variant) {.gdsync, name: "_on_body_entered".} =
  hide self
  discard self.hit()
  self.CollisionShape2D.setDeferred("disabled", variant true)

« Previous: Setting up the project

Next: Creating the enemy »

Clone this wiki locally