-
Notifications
You must be signed in to change notification settings - Fork 6
02.Coding the player
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.
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)