YourFirstGame with Haskell, Godot, and godot-haskell
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.
Godot-Haskell cheatsheet: I want to _?
Create a new project using godot-haskell
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- See godot-haskell for the godot-haskell repo.
- See Stack for more information on the Haskell build tool stack.
Create a NativeScript class to be used in Godot from Haskell
- Create a data type with instances of the NativeScript and HasBaseObject typeclasses. These emulate Godot’s Object Oriented architecture.
- HasBaseClass sets up the Godot Class that the data type inherits.
- NativeScript defines the methods, constructor, name, and signals of the class.
NOTEthis 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 []- Use
exportsandregisterClassmethods 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 @MyClassFind correct godot-haskell method to call
Here’s a suggested workflow:
- Look up function in Godot api at api. Keep in mind that GDScript allows default arguments in function calls and Haskell does not. This means that example calls in GDScript and Haskell may look different.
- Build/run local hoogle instance to search Haskell documentation for your project. This allows you to locate types and functions quickly in your project. You can search to locate the corresponding godot-haskell function.
- Alternatively you can build haddock documentation for godot-haskell and read through it in your browser. This will likely be more time intensive.
- See the following post for help with Hoogle/Haddock in Stack: https://lexi-lambda.github.io/blog/2018/02/10/an-opinionated-guide-to-haskell-in-2018/
Find the types expected by godot method
Here’s a tip you can use with a working IDE set up or a GHCi repl.
-- s is the obj you want to call the function on
let xyz = godot_func s then check the type of xyz to see what arguments it is expecting next.
Store some state in my NativeScript class that is updated during function calls
Use TVar to read / write the variable in conjunction with atomically.
- See STM for more info on STM (Software Transactional Memory) in Haskell (pay attention to Control.Concurrent.STM.TVar & Control.Monad.STM)
Convert between godot type and haskell types
- Use
fromLowLevel&toLowLevelfunctions
gameStr <- toLowLevel "start_game" :: IO GodotString- Use
fromGodotVariant/toVariantto convert to / from godot variants
message <- fromGodotVariant messageVt :: IO GodotString- Use
asNativeScriptto convert to NativeScript Haskell type
hud2 <- asNativeScript (safeCast hud2Node) :: IO (Maybe Hud2)Get a global godot class
Use godot_global_get_singleton
db <- Api.godot_global_get_singleton & withCString (unpack "ClassDB") >>= tryCast :: IO (Maybe Godot_ClassDB)Dynamically create an instance of a godot type
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"Dynamically load a resource
Use load on the global godot ResourceLoader (you may want to check that the resource exists via exists function first)
- NOTE: You’ll need to call
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 create an instance of one of my NativeScript types
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"Emit a godot signal
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 []Connect a godot signal
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 0Cast a GodotObject to another GodotType
Use one of tryCast, tryObjectCast, or safeCast
Get a godot child node
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)Use yield
I’ve used async in Haskell instead without issues so far. It’s not the same but it allows for asyncronous behaviour when needed.