Skip to content

Instantly share code, notes, and snippets.

@Double-oxygeN
Created January 21, 2024 07:14
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Double-oxygeN/0529a3b30348aad6f30d4f5912e021ad to your computer and use it in GitHub Desktop.
Save Double-oxygeN/0529a3b30348aad6f30d4f5912e021ad to your computer and use it in GitHub Desktop.
Godot 4 + F#: 2D game official tutorial
<Project Sdk="Godot.NET.Sdk/4.2.1">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>
<ItemGroup>
<Compile Include="TaskUtils.fs" />
<Compile Include="Player.fs" />
<Compile Include="Mob.fs" />
<Compile Include="HUD.fs" />
<Compile Include="Main.fs" />
</ItemGroup>
</Project>
using Godot;
public partial class HUD : FS.HUD
{
[Signal]
public delegate void StartGameEventHandler();
public override StringName StartGameSignal { get => SignalName.StartGame; }
public override void OnStartButtonPressed()
{
base.OnStartButtonPressed();
}
public override void OnMessageTimerTimeout()
{
base.OnMessageTimerTimeout();
}
}
namespace FS
open Godot
[<AbstractClass>]
type HUD () =
inherit CanvasLayer ()
// [<Signal>]
abstract StartGameSignal: StringName with get
member self.ShowMessage text =
let message = self.GetNode<Label>("Message")
message.Text <- text
message.Show()
self.GetNode<Timer>("MessageTimer").Start()
member self.ShowGameOver () =
task {
self.ShowMessage "Game Over"
let messageTimer = self.GetNode<Timer>("MessageTimer")
let! _ = self.ToSignal(messageTimer, Timer.SignalName.Timeout) |> TaskUtils.CriticalAwaiter
let message = self.GetNode<Label>("Message")
message.Text <- "Dodge the Creeps!"
message.Show()
let! _ = self.ToSignal(self.GetTree().CreateTimer(1.0), SceneTreeTimer.SignalName.Timeout) |> TaskUtils.CriticalAwaiter
self.GetNode<Button>("StartButton").Show()
}
member self.UpdateScore score =
self.GetNode<Label>("ScoreLabel").Text <- $"{score}"
abstract OnStartButtonPressed: unit -> unit
default self.OnStartButtonPressed () =
self.GetNode<Button>("StartButton").Hide()
self.EmitSignal(self.StartGameSignal) |> ignore
abstract OnMessageTimerTimeout: unit -> unit
default self.OnMessageTimerTimeout () =
self.GetNode<Label>("Message").Hide()
using Godot;
public partial class Main : FS.Main
{
[Export]
public override PackedScene MobScene { get; set; }
public override void _Ready()
{
base._Ready();
}
public override void OnScoreTimerTimeout()
{
base.OnScoreTimerTimeout();
}
public override void OnStartTimerTimeout()
{
base.OnStartTimerTimeout();
}
public override void OnMobTimerTimeout()
{
base.OnMobTimerTimeout();
}
public override void NewGame()
{
base.NewGame();
}
public override void GameOver()
{
base.GameOver();
}
}
namespace FS
open Godot
[<AbstractClass>]
type Main () =
inherit Node ()
let mutable Score = 0
// [<Export>]
abstract MobScene: PackedScene with get, set
abstract GameOver: unit -> unit
default self.GameOver () =
self.GetNode<Timer>("MobTimer").Stop()
self.GetNode<Timer>("ScoreTimer").Stop()
self.GetNode<HUD>("HUD").ShowGameOver() |> ignore
self.GetNode<AudioStreamPlayer>("Music").Stop()
self.GetNode<AudioStreamPlayer>("DeathSound").Play()
abstract NewGame: unit -> unit
default self.NewGame () =
Score <- 0
let player = self.GetNode<Player>("Player")
let startPosition = self.GetNode<Marker2D>("StartPosition")
player.Start(startPosition.Position)
self.GetNode<Timer>("StartTimer").Start()
let hud = self.GetNode<HUD>("HUD")
hud.UpdateScore(Score)
hud.ShowMessage("Get Ready!")
self.GetTree().CallGroup("mobs", Node.MethodName.QueueFree)
self.GetNode<AudioStreamPlayer>("Music").Play()
override self._Ready () =
()
abstract OnScoreTimerTimeout: unit -> unit
default self.OnScoreTimerTimeout () =
Score <- Score + 1
self.GetNode<HUD>("HUD").UpdateScore(Score)
abstract OnStartTimerTimeout: unit -> unit
default self.OnStartTimerTimeout () =
self.GetNode<Timer>("MobTimer").Start()
self.GetNode<Timer>("ScoreTimer").Start()
abstract OnMobTimerTimeout: unit -> unit
default self.OnMobTimerTimeout () =
let mob = self.MobScene.Instantiate<Mob>()
let mobSpawnLocation = self.GetNode<PathFollow2D>("MobPath/MobSpawnLocation")
mobSpawnLocation.ProgressRatio <- GD.Randf()
let randomAngle = GD.RandRange(float(-Mathf.Tau / 8.0f), float(Mathf.Tau / 8.0f)) |> float32
let direction = mobSpawnLocation.Rotation + Mathf.Tau / 4.0f + randomAngle
let velocity = Vector2(float32(GD.RandRange(150.0, 250.0)), 0.0f)
mob.Position <- mobSpawnLocation.Position
mob.Rotation <- direction
mob.LinearVelocity <- velocity.Rotated direction
self.AddChild mob
using Godot;
public partial class Mob : FS.Mob
{
public override void _Ready()
{
base._Ready();
}
public override void OnVisibleOnScreenNotifier2DScreenExited()
{
base.OnVisibleOnScreenNotifier2DScreenExited();
}
}
namespace FS
open Godot
[<AbstractClass>]
type Mob () =
inherit RigidBody2D ()
override self._Ready () =
let animatedSprite2D = self.GetNode<AnimatedSprite2D>("AnimatedSprite2D")
let mobTypes = animatedSprite2D.SpriteFrames.GetAnimationNames()
let randomMobTypeIndex = GD.Randi() % uint32 mobTypes.Length |> int
animatedSprite2D.Play(mobTypes[randomMobTypeIndex])
abstract OnVisibleOnScreenNotifier2DScreenExited: unit -> unit
default self.OnVisibleOnScreenNotifier2DScreenExited () =
self.QueueFree()
using Godot;
public partial class Player : FS.Player
{
[Signal]
public delegate void HitEventHandler();
public override StringName HitSignal { get => SignalName.Hit; }
[Export]
public override int Speed { get; set; } = 400;
public override void _Ready()
{
base._Ready();
}
public override void _Process(double delta)
{
base._Process(delta);
}
public override void OnBodyEntered(Node2D body)
{
base.OnBodyEntered(body);
}
}
namespace FS
open Godot
[<AbstractClass>]
type Player () =
inherit Area2D ()
let mutable ScreenSize = Vector2.Zero
// [<Signal>]
abstract HitSignal: StringName with get
// [<Export>]
abstract Speed: int with get, set
override self._Ready () =
ScreenSize <- self.GetViewportRect().Size
self.Hide()
override self._Process (delta: float) =
let velocity = Vector2(Input.GetActionStrength("ui_right") - Input.GetActionStrength("ui_left"), Input.GetActionStrength("ui_down") - Input.GetActionStrength("ui_up"))
let animatedSprite2D = self.GetNode<AnimatedSprite2D>("AnimatedSprite2D")
if velocity.Length() > 0.0f then
animatedSprite2D.Play()
else
animatedSprite2D.Stop()
self.Position <- self.Position + velocity.Normalized() * float32 self.Speed * float32 delta
|> _.Clamp(Vector2.Zero, ScreenSize)
if velocity.X <> 0.0f then
animatedSprite2D.Animation <- "walk"
animatedSprite2D.FlipV <- false
animatedSprite2D.FlipH <- velocity.X < 0.0f
elif velocity.Y <> 0.0f then
animatedSprite2D.Animation <- "up"
animatedSprite2D.FlipV <- velocity.Y > 0.0f
abstract OnBodyEntered: Node2D -> unit
default self.OnBodyEntered _ =
self.Hide()
self.EmitSignal(self.HitSignal) |> ignore
self.GetNode<CollisionShape2D>("CollisionShape2D").SetDeferred("disabled", true)
member self.Start (position: Vector2) =
self.Position <- position
self.Show()
self.GetNode<CollisionShape2D>("CollisionShape2D").Disabled <- false
namespace FS
open Godot
open System.Runtime.CompilerServices
module TaskUtils =
// In Godot, SignalAwaiter is actually not awaitable because it does not implement UnsafeOnCompleted,
// which is required by ICriticalNotifyCompletion.
// So we have to implement it ourselves.
//
// Example usage:
//
// task {
// let! _ = self.ToSignal(messageTimer, Timer.SignalName.Timeout) |> TaskUtils.CriticalAwaiter
// ()
// }
type CriticalAwaiter<'a, 'b when 'a :> IAwaitable<'b> and 'a :> IAwaiter<'b>>(awaiter: 'a) =
interface ICriticalNotifyCompletion with
member self.OnCompleted continuation = awaiter.OnCompleted continuation
member self.UnsafeOnCompleted continuation = awaiter.OnCompleted continuation
member self.IsCompleted
with get () = awaiter.IsCompleted
member self.GetResult () =
awaiter.GetResult ()
member self.GetAwaiter () =
self
<Project Sdk="Godot.NET.Sdk/4.2.1">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<TargetFramework Condition=" '$(GodotTargetPlatform)' == 'android' ">net7.0</TargetFramework>
<TargetFramework Condition=" '$(GodotTargetPlatform)' == 'ios' ">net8.0</TargetFramework>
<EnableDynamicLoading>true</EnableDynamicLoading>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="lib/FS.fsproj" />
</ItemGroup>
</Project>
@Double-oxygeN
Copy link
Author

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment