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.
    • 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 []
  • Use 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

Find correct godot-haskell method to call

Here’s a suggested workflow:

  1. 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.
  2. 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.

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 & toLowLevel functions
gameStr <- toLowLevel "start_game" :: IO GodotString
  • Use fromGodotVariant / toVariant to convert to / from godot variants
message <- fromGodotVariant messageVt :: IO GodotString
  • Use asNativeScript to 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 0

Cast 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.