This converts the YourFirstGame Godot tutorial to Haskell using godot-haskell library. Here is a link to the original tutorial: https://docs.godotengine.org/en/3.2/getting_started/step_by_step/your_first_game.html.
Here’s a link to the git repo containing the conversion: https://github.com/SpartanEngineer/godot3-dodge-haskell. It is mostly a 1-1 translation without relying too much on Haskell tricks. I won’t go over the specifics here since they’re already covered in the original tutorial. This uses version 3.2.1 of Godot.
Use stack new
with the template in the godot-haskell repo.
stack new myproject https://raw.githubusercontent.com/SimulaVR/godot-haskell/master/template/godot-haskell.hsfiles
NOTE
this uses Haskell TypeFamilies language extension._get_node' node name = get_node node `submerge` name >>= _tryCast'
data Mob2
= Mob2
{ _mob2_Base :: GodotRigidBody2D,
_mob2_MobType :: Text,
_mob2_Speed :: Float
}
instance HasBaseClass Mob2 where
type BaseClass Mob2 = GodotRigidBody2D
super = _mob2_Base
instance NativeScript Mob2 where
classInit base = pure $ Mob2 base "fly" 175
classMethods =
[ func NoRPC "_ready" $
\s _ -> do
animated <- _get_node' s "AnimatedSprite" :: IO GodotAnimatedSprite
toLowLevel (_mob2_MobType s) >>= set_animation animated,
func NoRPC "_on_Visibility_screen_exited" $
\s _ -> queue_free s,
func NoRPC "_on_start_game" $
\s _ -> queue_free s
]
You can also add signals via classSignals
and signal
method.
instance NativeScript Hud2 where
classInit base = -- ...
classMethods = -- ...
classSignals =
[ signal "start_game" [] -- [] is of type [(Text, GodotVariantType)]... this represents the args for the signal. Text = name of arg, GodotVariantType = type of arg.
]
You can call a signal via emit_signal
function.
-- emit_signal :: NativeScript a => a -> a -> [(Text, GodotVariantType)] -> IO GodotVariant
-- [(Text, GodotVariant)]... this represents the args for the signal. Text = name of arg, GodotVariantType = type of arg.
emit_signal s gameStr []
exports
and registerClass
methods to allow usage in Godot from the built libary.exports :: GdnativeHandle -> IO ()
exports desc = do
registerClass $ RegClass desc $ classInit @Player
registerClass $ RegClass desc $ classInit @Mob
-- ...
registerClass $ RegClass desc $ classInit @MyClass
Here’s a suggested workflow:
Here’s a tip you can use with a working IDE set up or a GHCi repl.
then check the type of xyz to see what arguments it is expecting next.
Use TVar
to read / write the variable in conjunction with atomically
.
fromLowLevel
& toLowLevel
functionsfromGodotVariant
/ toVariant
to convert to / from godot variantsasNativeScript
to convert to NativeScript Haskell typeUse godot_global_get_singleton
db <- Api.godot_global_get_singleton & withCString (unpack "ClassDB") >>= tryCast :: IO (Maybe Godot_ClassDB)
Use instance'
on the global godot ClassDB
db <- Api.godot_global_get_singleton & withCString (unpack "ClassDB") >>= tryCast :: IO (Maybe Godot_ClassDB)
case db of
Just classDb -> do
cName <- toLowLevel "RandomNumberGenerator" -- Name of the Godot class to make
cls <- instance' classDB cName >>= fromGodotVariant :: IO GodotObject
rng <- tryCast :: IO (Maybe GodotRandomNumberGenerator)
return (rng)
Nothing -> error "Unable to load global class db"
Use load on the global godot ResourceLoader (you may want to check that the resource exists via exists
function first)
instance'
on this to make a new instance of it.rlMaybe <- Api.godot_global_get_singleton & withCString (unpack "ResourceLoader") >>= tryCast :: IO (Maybe Godot_ResourceLoader)
case rlMaybe of
Just rl -> do
cName <- toLowLevel "PackedScene" -- Name of the Godot class to make
url <- toLowLevel "res://Mob2.tscn" -- Path to load
exist <- exists rl url clsName
case exist of
True -> do
r <- load rl url clsName False :: IO (Maybe GodotResource)
return (r)
False -> error "Unable to load class at the url inputted"
Nothing -> error "Unable to load global resource loader"
Dynamically load via the resource loader, create an instance of it, and then finally call asNativeScript
on it to convert it to your NativeScript type.
mobPackedSceneMaybe <- load' "PackedScene" "res://Mob2.tscn" >>= tryCast :: IO (Maybe GodotPackedScene)
case mobPackedSceneMaybe of
Just mobPackedScene -> do
mobObj <- instance' mobPackedScene 0
mob2 <- asNativeScript (safeCast mobObj) :: IO (Maybe Mob2)
return (mob2)
Nothing -> error "Unable to load: res://Mob2.tscn"
You can call a signal via emit_signal
function.
-- emit_signal :: NativeScript a => a -> a -> [(Text, GodotVariantType)] -> IO GodotVariant
-- [(Text, GodotVariant)]... this represents the args for the signal. Text = name of arg, GodotVariantType = type of arg.
emit_signal s gameStr []
You can connect a godot signal via connect
function.
-- connect :: NativeScript a => a -> GodotString -> GodotObject -> GodotString -> GodotArray -> Int -> IO Int
connect hud2 startGameStr (safeCast mob2) onStartGameStr gArr 0
Use one of tryCast
, tryObjectCast
, or safeCast
Use get_node
. This can take a path to a node as well (relative or absolute). See: https://docs.godotengine.org/en/stable/classes/class_node.html#class-node-method-get-node.
startPositionStr <- toLowLevel "StartPosition" :: IO GodotString
startPosition <- get_node s startPositionStr >>= tryCast :: IO (Maybe GodotPosition2D)
I’ve used async
in Haskell instead without issues so far. It’s not the same but it allows for asyncronous behaviour when needed.