Created
January 21, 2024 07:14
-
-
Save Double-oxygeN/0529a3b30348aad6f30d4f5912e021ad to your computer and use it in GitHub Desktop.
Godot 4 + F#: 2D game official tutorial
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using Godot; | |
public partial class Mob : FS.Mob | |
{ | |
public override void _Ready() | |
{ | |
base._Ready(); | |
} | |
public override void OnVisibleOnScreenNotifier2DScreenExited() | |
{ | |
base.OnVisibleOnScreenNotifier2DScreenExited(); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<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> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
See: https://zenn.dev/double_oxygen/articles/8a6a51fc677ba9