Skip to content

Commit 3c576fa

Browse files
committed
fix: particles invisible due to Rlgl/BeginMode2D conflict and pipeline ordering bug
- GridOccluders.fromCellGrid: replace hardcoded 'skip top edges' platformer assumption with EdgeFlags parameter so callers choose which edges occlude - ParticleCommand.Render: swap broken Rlgl immediate mode for raylib DrawTexturePro which properly respects BeginMode2D camera transform - particleSystem: move confetti spawn + jump sound into physicsSystem where jump is actually processed, avoiding pipeline ordering bug where IsGrounded was already false by the time particles ran
1 parent 1a31e01 commit 3c576fa

4 files changed

Lines changed: 124 additions & 128 deletions

File tree

samples/PlatformerSample/Systems.fs

Lines changed: 41 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,16 @@ open PlatformerSample.WorldGen
2020
let nearbyPlatforms = ResizeArray<Rectangle>(256)
2121
let keysToRemove = ResizeArray<struct (int * int)>(32)
2222

23+
let confettiColors =
24+
[| Color(255uy, 50uy, 50uy, 255uy)
25+
Color(50uy, 255uy, 50uy, 255uy)
26+
Color(50uy, 50uy, 255uy, 255uy)
27+
Color(255uy, 255uy, 50uy, 255uy)
28+
Color(255uy, 50uy, 255uy, 255uy)
29+
Color(50uy, 255uy, 255uy, 255uy)
30+
Color(255uy, 150uy, 50uy, 255uy)
31+
Color(255uy, 50uy, 150uy, 255uy) |]
32+
2333
// -------------------------------------------------------------
2434
// System: Input -> Movement Intent
2535
// -------------------------------------------------------------
@@ -46,6 +56,35 @@ let physicsSystem (dt: float32) (model: Model) : struct (Model * Cmd<Msg>) =
4656

4757
let velocityY =
4858
if jumpPressed && canJump then
59+
// Spawn confetti burst
60+
let rng = System.Random.Shared
61+
let mutable pc = model.ParticleCount
62+
let particles = model.Particles
63+
let particleVelocities = model.ParticleVelocities
64+
for i = 0 to 19 do
65+
if pc < particles.Length then
66+
let spawnPos =
67+
model.PlayerPosition
68+
+ Vector2(
69+
playerWidth / 2.0f + float32(rng.NextDouble() * 20.0 - 10.0),
70+
playerHeight * 0.3f
71+
)
72+
particles[pc] <- {
73+
Position = spawnPos
74+
Size = Vector2(4.0f, 4.0f)
75+
Rotation = float32(rng.NextDouble() * Math.PI * 2.0)
76+
SourceRect = Rectangle(0.0f, 0.0f, 1.0f, 1.0f)
77+
Color = confettiColors[rng.Next(confettiColors.Length)]
78+
}
79+
particleVelocities[pc] <-
80+
Vector2(
81+
float32(rng.NextDouble() * 300.0 - 150.0),
82+
float32(rng.NextDouble() * -250.0 - 50.0)
83+
)
84+
pc <- pc + 1
85+
model.ParticleCount <- pc
86+
Raylib.PlaySound(model.Assets.JumpSound)
87+
4988
jumpSpeed
5089
else
5190
model.PlayerVelocity.Y + gravity * dt
@@ -168,53 +207,21 @@ let animationSystem (dt: float32) (model: Model) : struct (Model * Cmd<Msg>) =
168207
// -------------------------------------------------------------
169208

170209
let particleSystem (dt: float32) (model: Model) : struct (Model * Cmd<Msg>) =
171-
let mutable playedJumpSound =
172-
model.Actions.Started.Contains(GameAction.Jump) && model.IsGrounded
173-
174210
let particles = model.Particles
175211
let particleVelocities = model.ParticleVelocities
176212
let mutable particleCount = model.ParticleCount
177213

178-
// Spawn burst on jump
179-
if playedJumpSound then
180-
let rng = System.Random.Shared
181-
182-
for i = 0 to 11 do
183-
if particleCount < particles.Length then
184-
particles[particleCount] <- {
185-
Position =
186-
model.PlayerPosition + Vector2(playerWidth / 2.0f, playerHeight)
187-
Size = Vector2(8.0f, 8.0f)
188-
Rotation = float32(rng.NextDouble() * Math.PI * 2.0)
189-
SourceRect = Rectangle(0.0f, 0.0f, 1.0f, 1.0f)
190-
Color = Color(255uy, 255uy, 0uy, 255uy)
191-
}
192-
193-
particleVelocities[particleCount] <-
194-
Vector2(
195-
float32(rng.NextDouble() * 200.0 - 100.0),
196-
float32(rng.NextDouble() * -150.0 - 50.0)
197-
)
198-
199-
particleCount <- particleCount + 1
200-
201-
// Update existing particles
202214
for i = 0 to particleCount - 1 do
203215
let vel = particleVelocities[i]
204-
let newVel = Vector2(vel.X, vel.Y + gravity * dt * 0.3f)
216+
let newVel = Vector2(vel.X, vel.Y + gravity * dt * 0.05f)
205217
particleVelocities[i] <- newVel
206-
207218
particles[i] <- {
208219
particles[i] with
209220
Position = particles[i].Position + newVel * dt
210221
}
211222

212-
ParticleSimulation.fadeAndCompact particles &particleCount 255.0f dt
223+
ParticleSimulation.fadeAndCompact particles &particleCount 60.0f dt
213224
model.ParticleCount <- particleCount
214-
215-
if playedJumpSound then
216-
Raylib.PlaySound(model.Assets.JumpSound)
217-
218225
model, Cmd.none
219226

220227
// -------------------------------------------------------------

samples/PlatformerSample/WorldGen.fs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,8 @@ let generateChunk (cx: int) (cy: int) (worldSeed: int) : Chunk =
127127
let platforms = extractPlatforms grid
128128
let torches = extractTorches grid rng
129129
// Only floating platforms cast shadows — ground does not
130-
let occluders = GridOccluders.fromCellGrid (fun t -> t = Platform) grid
130+
let occluders =
131+
GridOccluders.fromCellGrid (fun t -> t = Platform) (GridOccluders.Edge.Bottom ||| GridOccluders.Edge.Left ||| GridOccluders.Edge.Right) grid
131132

132133
{
133134
Grid = grid

src/Mibo.Raylib/Graphics2D/Lighting/GridOccluders.fs

Lines changed: 68 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,25 @@ open Mibo.Layout
1010
module GridOccluders =
1111

1212
/// <summary>
13-
/// Generates <see cref="Occluder2D"/> line segments for every exposed edge
14-
/// of solid cells in a <see cref="CellGrid2D"/>.
13+
/// Flags specifying which edges of a grid cell should generate shadow-casting occluders.
14+
/// </summary>
15+
[<Flags>]
16+
type Edge =
17+
| None = 0
18+
| Top = 1
19+
| Bottom = 2
20+
| Left = 4
21+
| Right = 8
22+
| All = 15
23+
24+
/// <summary>
25+
/// Generates <see cref="Occluder2D"/> line segments for exposed edges of solid cells
26+
/// in a <see cref="CellGrid2D"/>, filtering to only the requested <paramref name="edges"/>.
1527
/// </summary>
1628
/// <param name="isSolid">Predicate that returns true for solid/obstacle cell contents.</param>
29+
/// <param name="edges">Which cell edges may produce occluders (e.g. <c>Edge.Bottom ||| Edge.Left ||| Edge.Right</c> for platformers, <c>Edge.All</c> for top-down).</param>
1730
/// <param name="grid">The grid to scan.</param>
18-
let fromCellGrid (isSolid: 'T -> bool) (grid: CellGrid2D<'T>) : Occluder2D[] =
31+
let fromCellGrid (isSolid: 'T -> bool) (edges: Edge) (grid: CellGrid2D<'T>) : Occluder2D[] =
1932
let occluders = ResizeArray<Occluder2D>()
2033
let cellW = grid.CellSize.X
2134
let cellH = grid.CellSize.Y
@@ -29,63 +42,80 @@ module GridOccluders =
2942
let wx = grid.Origin.X + float32 x * cellW
3043
let wy = grid.Origin.Y + float32 y * cellH
3144

32-
// NOTE: We intentionally do NOT generate top-edge occluders.
33-
// In a 2D platformer, the top surface of tiles is the ground
34-
// the player stands on — it must receive light from above.
35-
// Shadow casting is handled by bottom, left, and right edges.
36-
3745
// Bottom edge
38-
match CellGrid2D.get x (y + 1) grid with
39-
| ValueNone ->
40-
occluders.Add(
41-
{
42-
P1 = Vector2(wx, wy + cellH)
43-
P2 = Vector2(wx + cellW, wy + cellH)
44-
}
45-
)
46-
| ValueSome neighbor ->
47-
if not(isSolid neighbor) then
46+
if edges &&& Edge.Bottom = Edge.Bottom then
47+
match CellGrid2D.get x (y + 1) grid with
48+
| ValueNone ->
4849
occluders.Add(
4950
{
5051
P1 = Vector2(wx, wy + cellH)
5152
P2 = Vector2(wx + cellW, wy + cellH)
5253
}
5354
)
55+
| ValueSome neighbor ->
56+
if not (isSolid neighbor) then
57+
occluders.Add(
58+
{
59+
P1 = Vector2(wx, wy + cellH)
60+
P2 = Vector2(wx + cellW, wy + cellH)
61+
}
62+
)
63+
64+
// Top edge
65+
if edges &&& Edge.Top = Edge.Top then
66+
match CellGrid2D.get x (y - 1) grid with
67+
| ValueNone ->
68+
occluders.Add(
69+
{
70+
P1 = Vector2(wx, wy)
71+
P2 = Vector2(wx + cellW, wy)
72+
}
73+
)
74+
| ValueSome neighbor ->
75+
if not (isSolid neighbor) then
76+
occluders.Add(
77+
{
78+
P1 = Vector2(wx, wy)
79+
P2 = Vector2(wx + cellW, wy)
80+
}
81+
)
5482

5583
// Left edge
56-
match CellGrid2D.get (x - 1) y grid with
57-
| ValueNone ->
58-
occluders.Add(
59-
{
60-
P1 = Vector2(wx, wy)
61-
P2 = Vector2(wx, wy + cellH)
62-
}
63-
)
64-
| ValueSome neighbor ->
65-
if not(isSolid neighbor) then
84+
if edges &&& Edge.Left = Edge.Left then
85+
match CellGrid2D.get (x - 1) y grid with
86+
| ValueNone ->
6687
occluders.Add(
6788
{
6889
P1 = Vector2(wx, wy)
6990
P2 = Vector2(wx, wy + cellH)
7091
}
7192
)
93+
| ValueSome neighbor ->
94+
if not (isSolid neighbor) then
95+
occluders.Add(
96+
{
97+
P1 = Vector2(wx, wy)
98+
P2 = Vector2(wx, wy + cellH)
99+
}
100+
)
72101

73102
// Right edge
74-
match CellGrid2D.get (x + 1) y grid with
75-
| ValueNone ->
76-
occluders.Add(
77-
{
78-
P1 = Vector2(wx + cellW, wy)
79-
P2 = Vector2(wx + cellW, wy + cellH)
80-
}
81-
)
82-
| ValueSome neighbor ->
83-
if not(isSolid neighbor) then
103+
if edges &&& Edge.Right = Edge.Right then
104+
match CellGrid2D.get (x + 1) y grid with
105+
| ValueNone ->
84106
occluders.Add(
85107
{
86108
P1 = Vector2(wx + cellW, wy)
87109
P2 = Vector2(wx + cellW, wy + cellH)
88110
}
89111
)
112+
| ValueSome neighbor ->
113+
if not (isSolid neighbor) then
114+
occluders.Add(
115+
{
116+
P1 = Vector2(wx + cellW, wy)
117+
P2 = Vector2(wx + cellW, wy + cellH)
118+
}
119+
)
90120

91121
occluders.ToArray()

src/Mibo.Raylib/Graphics2D/Lighting/Particle.fs

Lines changed: 13 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -39,64 +39,22 @@ type ParticleCommand
3939
interface IRenderCommand2D with
4040
member _.Layer = cmdLayer
4141

42-
member _.Render ctx =
42+
member _.Render _ =
4343
let tex = texture
4444
let ps = particles
4545
let c = count
46-
let invW = 1.0f / float32 tex.Width
47-
let invH = 1.0f / float32 tex.Height
48-
49-
ctx.DrawImmediate(fun () ->
50-
Rlgl.SetTexture(tex.Id)
51-
Rlgl.Begin(DrawMode.Quads)
52-
53-
for i = 0 to c - 1 do
54-
let p = ps[i]
55-
56-
let src = p.SourceRect
57-
let u0 = src.X * invW
58-
let v0 = src.Y * invH
59-
let u1 = (src.X + src.Width) * invW
60-
let v1 = (src.Y + src.Height) * invH
61-
62-
let halfW = p.Size.X * 0.5f
63-
let halfH = p.Size.Y * 0.5f
64-
65-
let radians = p.Rotation * MathF.PI / 180.0f
66-
let cosA = MathF.Cos(radians)
67-
let sinA = MathF.Sin(radians)
68-
69-
let rotX x y = cosA * x - sinA * y
70-
let rotY x y = sinA * x + cosA * y
71-
72-
let tlX = p.Position.X + rotX -halfW -halfH
73-
let tlY = p.Position.Y + rotY -halfW -halfH
74-
let trX = p.Position.X + rotX halfW -halfH
75-
let trY = p.Position.Y + rotY halfW -halfH
76-
let brX = p.Position.X + rotX halfW halfH
77-
let brY = p.Position.Y + rotY halfW halfH
78-
let blX = p.Position.X + rotX -halfW halfH
79-
let blY = p.Position.Y + rotY -halfW halfH
80-
81-
let r = p.Color.R
82-
let g = p.Color.G
83-
let b = p.Color.B
84-
let a = p.Color.A
85-
86-
Rlgl.Color4ub(r, g, b, a)
87-
Rlgl.TexCoord2f(u0, v0)
88-
Rlgl.Vertex2f(tlX, tlY)
89-
Rlgl.Color4ub(r, g, b, a)
90-
Rlgl.TexCoord2f(u1, v0)
91-
Rlgl.Vertex2f(trX, trY)
92-
Rlgl.Color4ub(r, g, b, a)
93-
Rlgl.TexCoord2f(u1, v1)
94-
Rlgl.Vertex2f(brX, brY)
95-
Rlgl.Color4ub(r, g, b, a)
96-
Rlgl.TexCoord2f(u0, v1)
97-
Rlgl.Vertex2f(blX, blY)
98-
99-
Rlgl.End())
46+
47+
for i = 0 to c - 1 do
48+
let p = ps[i]
49+
let halfW = p.Size.X * 0.5f
50+
let halfH = p.Size.Y * 0.5f
51+
let dest = Rectangle(
52+
p.Position.X - halfW,
53+
p.Position.Y - halfH,
54+
p.Size.X,
55+
p.Size.Y
56+
)
57+
Raylib.DrawTexturePro(tex, p.SourceRect, dest, Vector2.Zero, p.Rotation, p.Color)
10058

10159
/// <summary>Factory functions for particle render commands.</summary>
10260
module ParticleCommands =

0 commit comments

Comments
 (0)