171 lines
8.1 KiB
Plaintext
171 lines
8.1 KiB
Plaintext
;;;; raylib [textures] example - fog of war
|
|
;;;;
|
|
;;;; examples/textures/textures_fog_of_war.c. Picked because it is the only
|
|
;;;; example in the whole upstream tree where a *texture filter* is the point
|
|
;;;; rather than a detail. The fog is drawn into a render texture one pixel
|
|
;;;; per tile — 25 by 15 — and then stretched to 800x450 on the way to the
|
|
;;;; screen, and the smooth edge between seen and unseen is entirely the
|
|
;;;; bilinear sampler doing the interpolation. With the default
|
|
;;;; :filter-point the same program draws 32-pixel squares.
|
|
;;;;
|
|
;;;; That is what needed adding: `TextureFilter`, a new enum in
|
|
;;;; vendor/raylib/raylib.fln, with set-texture-filter moved from the
|
|
;;;; generated half to the hand-written one and mapped in `bindings` so its
|
|
;;;; six members are checked against raylib.h. The header says `int filter`
|
|
;;;; and there is nothing in an `int` to say that 1 is the interesting value;
|
|
;;;; `:filter-bilinear` says it.
|
|
;;;;
|
|
;;;; The three other things it exercises, none of which needed a line:
|
|
;;;;
|
|
;;;; - A render texture used as a *scratch surface* rather than as a
|
|
;;;; post-processing pass. core-scissor-test clipped the real framebuffer;
|
|
;;;; this draws a whole second frame into a 25x15 target and then samples
|
|
;;;; it. begin-texture-mode/end-texture-mode were already bound.
|
|
;;;; - draw-texture-pro with a NEGATIVE source height. A render texture's
|
|
;;;; rows come out of GL bottom-up, so every program that samples one has
|
|
;;;; to flip it, and raylib's convention for that is a source rectangle
|
|
;;;; with a negative height rather than a separate flag. Nothing in the
|
|
;;;; corpus had passed one. A Rectangle whose `height` field had been
|
|
;;;; permuted with `width` would draw the fog mirrored and the wrong way
|
|
;;;; up at once.
|
|
;;;; - clear-background with `blank` — alpha 0 — inside a texture mode, so
|
|
;;;; the untouched parts of the fog target are transparent and the map
|
|
;;;; shows through.
|
|
;;;;
|
|
;;;; The C's Map struct with its two calloc'd `unsigned char *` is two `[375
|
|
;;;; u8]` globals here. The dimensions are compile-time constants in the C
|
|
;;;; too (the #defines and the two assignments right after), so nothing is
|
|
;;;; lost by fixing them, and a global cannot hold a Vec in any case
|
|
;;;; (docs/PORTING.md §3). 375 is 25*15, written out because Flan's array length
|
|
;;;; must be a literal.
|
|
|
|
import rl "vendor:raylib"
|
|
|
|
import d "digits.fln"
|
|
|
|
const screen-width = 800
|
|
const screen-height = 450
|
|
|
|
const map-tile-size = 32
|
|
const player-size = 16
|
|
|
|
;; How far the player sees, in tiles. The C's loop runs from -this to +this
|
|
;; exclusive, so the lit patch is 2*this tiles across and not 2*this+1 — an
|
|
;; asymmetry in the original that is kept rather than tidied, because tidying
|
|
;; it would change the picture.
|
|
const player-tile-visibility = 2
|
|
|
|
const tiles-x = 25
|
|
const tiles-y = 15
|
|
|
|
;; 0 or 1, picked once: which of the two blues a tile is drawn in.
|
|
once tile-ids: [375 u8]
|
|
|
|
;; 0 = never seen (solid black), 1 = visible now (no fog), 2 = seen before
|
|
;; (mostly black). The three-way state is why this is a byte per tile and not
|
|
;; a bit.
|
|
once tile-fog: [375 u8]
|
|
|
|
once player-position: rl/Vector2
|
|
once player-tile-x: i32
|
|
once player-tile-y: i32
|
|
|
|
once fog-of-war: rl/RenderTexture2D
|
|
|
|
fn main() -> ()
|
|
rl/init-window(screen-width, screen-height,
|
|
"raylib [textures] example - fog of war")
|
|
defer rl/close-window()
|
|
tile-ids = array(375, u8)
|
|
tile-fog = array(375, u8)
|
|
for i in range(tiles-x * tiles-y)
|
|
tile-ids[i] = u8(rl/get-random-value(0, 1))
|
|
player-position = rl/Vector2{.x 180.0 .y 130.0}
|
|
player-tile-x = 0
|
|
player-tile-y = 0
|
|
;; One texel per tile. The stretch to full size on draw is where the
|
|
;; smoothing happens, which is why the target is this small on purpose.
|
|
fog-of-war = rl/load-render-texture(tiles-x, tiles-y)
|
|
defer rl/unload-render-texture(fog-of-war)
|
|
rl/set-texture-filter(fog-of-war.texture, :filter-bilinear)
|
|
rl/set-target-fps(60)
|
|
until rl/window-should-close()
|
|
;; Update
|
|
if rl/is-key-down(:key-right)
|
|
player-position.x += 5.0
|
|
if rl/is-key-down(:key-left)
|
|
player-position.x -= 5.0
|
|
if rl/is-key-down(:key-down)
|
|
player-position.y += 5.0
|
|
if rl/is-key-down(:key-up)
|
|
player-position.y -= 5.0
|
|
;; Keep the player inside the tilemap. The far edge is measured against
|
|
;; the player's far side, which is why player-size is subtracted.
|
|
let max-x = f32(tiles-x * map-tile-size - player-size)
|
|
let max-y = f32(tiles-y * map-tile-size - player-size)
|
|
player-position.x = clamp(player-position.x, 0.0, max-x)
|
|
player-position.y = clamp(player-position.y, 0.0, max-y)
|
|
;; Everything lit on the previous frame drops to "seen before". The lit
|
|
;; tiles are then set back to 1 below, so a tile still in view never
|
|
;; spends a frame dimmed.
|
|
for i in range(tiles-x * tiles-y)
|
|
if tile-fog[i] == 1
|
|
tile-fog[i] = 2
|
|
;; Which tile the player's centre is standing on.
|
|
player-tile-x = i32((player-position.x + f32(map-tile-size / 2)) / f32(map-tile-size))
|
|
player-tile-y = i32((player-position.y + f32(map-tile-size / 2)) / f32(map-tile-size))
|
|
;; Light the square around the player, skipping anything off the map.
|
|
;; Without that test this reads and writes outside the array, which in the
|
|
;; C is undefined and here is a bounds trap — the same bug, reported.
|
|
let vis-y = player-tile-y - player-tile-visibility
|
|
while vis-y < player-tile-y + player-tile-visibility
|
|
let x = player-tile-x - player-tile-visibility
|
|
while x < player-tile-x + player-tile-visibility
|
|
if x >= 0 and x < tiles-x and vis-y >= 0 and vis-y < tiles-y
|
|
tile-fog[vis-y * tiles-x + x] = 1
|
|
x += 1
|
|
vis-y += 1
|
|
;; Draw the fog into its own little target first, at one pixel per tile.
|
|
;; blank is alpha 0, so a tile that is neither unseen nor remembered
|
|
;; leaves nothing behind and the map below shows through unmodified.
|
|
rl/with-texture-mode(fog-of-war):
|
|
rl/clear-background(rl/blank)
|
|
for y in range(tiles-y)
|
|
for x in range(tiles-x)
|
|
;; A tile in view (1) draws nothing at all and stays transparent.
|
|
let f = tile-fog[y * tiles-x + x]
|
|
if f != 1
|
|
rl/draw-rectangle(x, y, 1, 1,
|
|
if f == 0 then rl/black else rl/fade(rl/black, 0.8))
|
|
rl/with-drawing:
|
|
rl/clear-background(rl/raywhite)
|
|
;; The map itself, in full size.
|
|
for y in range(tiles-y)
|
|
for x in range(tiles-x)
|
|
rl/draw-rectangle(x * map-tile-size, y * map-tile-size, map-tile-size,
|
|
map-tile-size,
|
|
if tile-ids[y * tiles-x + x] == 0 then rl/blue else rl/fade(rl/blue, 0.9))
|
|
rl/draw-rectangle-lines(x * map-tile-size, y * map-tile-size,
|
|
map-tile-size, map-tile-size,
|
|
rl/fade(rl/darkblue, 0.5))
|
|
rl/draw-rectangle-v(player-position,
|
|
rl/Vector2{.x f32(player-size) .y f32(player-size)},
|
|
rl/red)
|
|
;; The fog, stretched over the whole map. The negative source height is
|
|
;; the flip — see the header comment.
|
|
rl/draw-texture-pro(fog-of-war.texture,
|
|
rl/Rectangle{.x 0.0, .y 0.0, .width f32(fog-of-war.texture.width), .height 0.0 - f32(fog-of-war.texture.height)},
|
|
rl/Rectangle{.x 0.0 .y 0.0 .width f32(tiles-x * map-tile-size) .height f32(tiles-y * map-tile-size)},
|
|
rl/Vector2{.x 0.0 .y 0.0}, 0.0, rl/white)
|
|
;; The C's TextFormat("Current tile: [%i,%i]", ...). Each number is drawn
|
|
;; before the next is formatted, which examples/digits.fln requires:
|
|
;; both share one static buffer in the runtime.
|
|
let x = 10
|
|
x += d/draw-piece("Current tile: [", x, 10, 20, rl/raywhite)
|
|
x += d/draw-int(player-tile-x, x, 10, 20, rl/raywhite)
|
|
x += d/draw-piece(",", x, 10, 20, rl/raywhite)
|
|
x += d/draw-int(player-tile-y, x, 10, 20, rl/raywhite)
|
|
d/draw-piece("]", x, 10, 20, rl/raywhite)
|
|
rl/draw-text("ARROW KEYS to move", 10, screen-height - 25, 20,
|
|
rl/raywhite)
|