272 lines
6.7 KiB
Markdown
272 lines
6.7 KiB
Markdown
# quick reference
|
|
|
|
## fixed-point math
|
|
|
|
32.32 format: 32 integer bits, 32 fractional bits.
|
|
|
|
```zig
|
|
const Fixed = struct { raw: i64 };
|
|
|
|
// constants
|
|
const ONE = Fixed{ .raw = 1 << 32 };
|
|
const HALF = Fixed{ .raw = 1 << 31 };
|
|
const PI = Fixed{ .raw = 13493037705 }; // pi * 2^32
|
|
|
|
// from int
|
|
Fixed{ .raw = @as(i64, n) << 32 }
|
|
|
|
// to float (rendering only!)
|
|
@as(f32, @floatFromInt(f.raw)) / 4294967296.0
|
|
|
|
// add/sub: direct
|
|
a.raw + b.raw
|
|
a.raw - b.raw
|
|
|
|
// mul: widen to i128
|
|
@intCast((@as(i128, a.raw) * b.raw) >> 32)
|
|
|
|
// div: shift first
|
|
@intCast(@divTrunc(@as(i128, a.raw) << 32, b.raw))
|
|
```
|
|
|
|
## trig lookup tables
|
|
|
|
generate at comptime:
|
|
|
|
```zig
|
|
const TABLE_SIZE = 1024;
|
|
const sin_table: [TABLE_SIZE]Fixed = blk: {
|
|
var table: [TABLE_SIZE]Fixed = undefined;
|
|
for (0..TABLE_SIZE) |i| {
|
|
const angle = @as(f64, @floatFromInt(i)) * std.math.pi * 2.0 / TABLE_SIZE;
|
|
const s = @sin(angle);
|
|
table[i] = .{ .raw = @intFromFloat(s * 4294967296.0) };
|
|
}
|
|
break :blk table;
|
|
};
|
|
|
|
pub fn sin(angle: Fixed) Fixed {
|
|
// angle in radians, normalize to table index
|
|
const two_pi = Fixed{ .raw = 26986075409 }; // 2*pi * 2^32
|
|
var a = @mod(angle.raw, two_pi.raw);
|
|
if (a < 0) a += two_pi.raw;
|
|
const idx = @as(usize, @intCast((a * TABLE_SIZE) >> 32)) % TABLE_SIZE;
|
|
return sin_table[idx];
|
|
}
|
|
|
|
pub fn cos(angle: Fixed) Fixed {
|
|
const half_pi = Fixed{ .raw = 6746518852 }; // pi/2 * 2^32
|
|
return sin(Fixed{ .raw = angle.raw + half_pi.raw });
|
|
}
|
|
```
|
|
|
|
## networking
|
|
|
|
### packet format
|
|
|
|
```
|
|
byte 0: packet type
|
|
bytes 1-4: frame number (u32 little-endian)
|
|
byte 5: player id
|
|
bytes 6-9: checksum (u32 little-endian)
|
|
bytes 10+: payload
|
|
```
|
|
|
|
### packet types
|
|
|
|
| type | id | payload |
|
|
|------|------|---------|
|
|
| INPUT | 0x01 | move(i8), angle_delta(i8), power_delta(i8), fire(u8) |
|
|
| SYNC | 0x02 | full GameState blob |
|
|
| PING | 0x03 | timestamp(u64) |
|
|
| PONG | 0x04 | original timestamp(u64) |
|
|
|
|
### zig udp basics
|
|
|
|
```zig
|
|
const std = @import("std");
|
|
const net = std.net;
|
|
|
|
// create socket
|
|
const sock = try std.posix.socket(std.posix.AF.INET, std.posix.SOCK.DGRAM, 0);
|
|
defer std.posix.close(sock);
|
|
|
|
// bind (server)
|
|
const addr = net.Address.initIp4(.{ 0, 0, 0, 0 }, 7777);
|
|
try std.posix.bind(sock, &addr.any, addr.getLen());
|
|
|
|
// send
|
|
const dest = net.Address.initIp4(.{ 127, 0, 0, 1 }, 7777);
|
|
_ = try std.posix.sendto(sock, &packet_bytes, 0, &dest.any, dest.getLen());
|
|
|
|
// receive
|
|
var buf: [1024]u8 = undefined;
|
|
var src_addr: std.posix.sockaddr = undefined;
|
|
var src_len: std.posix.socklen_t = @sizeOf(std.posix.sockaddr);
|
|
const len = try std.posix.recvfrom(sock, &buf, 0, &src_addr, &src_len);
|
|
```
|
|
|
|
## raylib-zig setup
|
|
|
|
### build.zig.zon
|
|
|
|
```zig
|
|
.{
|
|
.name = "lockstep",
|
|
.version = "0.0.1",
|
|
.dependencies = .{
|
|
.raylib_zig = .{
|
|
.url = "git+https://github.com/Not-Nik/raylib-zig#devel",
|
|
.hash = "...", // zig fetch will tell you
|
|
},
|
|
},
|
|
}
|
|
```
|
|
|
|
### build.zig
|
|
|
|
```zig
|
|
const std = @import("std");
|
|
|
|
pub fn build(b: *std.Build) void {
|
|
const target = b.standardTargetOptions(.{});
|
|
const optimize = b.standardOptimizeOption(.{});
|
|
|
|
const raylib_dep = b.dependency("raylib_zig", .{
|
|
.target = target,
|
|
.optimize = optimize,
|
|
});
|
|
|
|
const exe = b.addExecutable(.{
|
|
.name = "lockstep",
|
|
.root_module = b.createModule(.{
|
|
.root_source_file = b.path("src/main.zig"),
|
|
.target = target,
|
|
.optimize = optimize,
|
|
}),
|
|
});
|
|
|
|
exe.root_module.addImport("raylib", raylib_dep.module("raylib"));
|
|
exe.linkLibrary(raylib_dep.artifact("raylib"));
|
|
|
|
b.installArtifact(exe);
|
|
|
|
const run_cmd = b.addRunArtifact(exe);
|
|
run_cmd.step.dependOn(b.getInstallStep());
|
|
const run_step = b.step("run", "run the game");
|
|
run_step.dependOn(&run_cmd.step);
|
|
}
|
|
```
|
|
|
|
### basic window
|
|
|
|
```zig
|
|
const rl = @import("raylib");
|
|
|
|
pub fn main() !void {
|
|
rl.initWindow(800, 600, "lockstep");
|
|
defer rl.closeWindow();
|
|
rl.setTargetFPS(60);
|
|
|
|
while (!rl.windowShouldClose()) {
|
|
rl.beginDrawing();
|
|
defer rl.endDrawing();
|
|
rl.clearBackground(rl.Color.black);
|
|
rl.drawText("hello", 10, 10, 20, rl.Color.white);
|
|
}
|
|
}
|
|
```
|
|
|
|
## rendering glow effect
|
|
|
|
### render texture + shader
|
|
|
|
```zig
|
|
const rl = @import("raylib");
|
|
|
|
// load shader
|
|
const blur_shader = rl.loadShader(null, "shaders/blur.fs");
|
|
defer rl.unloadShader(blur_shader);
|
|
|
|
// create render textures
|
|
const game_tex = rl.loadRenderTexture(800, 600);
|
|
const blur_tex = rl.loadRenderTexture(800, 600);
|
|
defer rl.unloadRenderTexture(game_tex);
|
|
defer rl.unloadRenderTexture(blur_tex);
|
|
|
|
// in game loop:
|
|
|
|
// 1. draw game to texture
|
|
rl.beginTextureMode(game_tex);
|
|
rl.clearBackground(.{ .r = 10, .g = 10, .b = 18, .a = 255 });
|
|
drawGame(&state);
|
|
rl.endTextureMode();
|
|
|
|
// 2. blur pass (horizontal)
|
|
rl.beginTextureMode(blur_tex);
|
|
rl.beginShaderMode(blur_shader);
|
|
// set direction uniform to (1, 0)
|
|
rl.drawTextureRec(game_tex.texture, .{ .x = 0, .y = 0, .width = 800, .height = -600 }, .{ .x = 0, .y = 0 }, .white);
|
|
rl.endShaderMode();
|
|
rl.endTextureMode();
|
|
|
|
// 3. blur pass (vertical) + composite
|
|
rl.beginDrawing();
|
|
// draw original
|
|
rl.drawTextureRec(game_tex.texture, .{ .x = 0, .y = 0, .width = 800, .height = -600 }, .{ .x = 0, .y = 0 }, .white);
|
|
// additive blend blurred
|
|
rl.beginBlendMode(.additive);
|
|
rl.beginShaderMode(blur_shader);
|
|
// set direction uniform to (0, 1)
|
|
rl.drawTextureRec(blur_tex.texture, .{ .x = 0, .y = 0, .width = 800, .height = -600 }, .{ .x = 0, .y = 0 }, .white);
|
|
rl.endShaderMode();
|
|
rl.endBlendMode();
|
|
rl.endDrawing();
|
|
```
|
|
|
|
### drawing lines
|
|
|
|
```zig
|
|
// basic line
|
|
rl.drawLine(x1, y1, x2, y2, color);
|
|
|
|
// thick line
|
|
rl.drawLineEx(.{ .x = x1, .y = y1 }, .{ .x = x2, .y = y2 }, thickness, color);
|
|
|
|
// terrain as connected lines
|
|
for (0..SCREEN_WIDTH - 1) |x| {
|
|
const y1 = SCREEN_HEIGHT - terrain.heights[x].toInt();
|
|
const y2 = SCREEN_HEIGHT - terrain.heights[x + 1].toInt();
|
|
rl.drawLine(@intCast(x), y1, @intCast(x + 1), y2, .{ .r = 0, .g = 255, .b = 0, .a = 255 });
|
|
}
|
|
```
|
|
|
|
## checksum
|
|
|
|
```zig
|
|
pub fn checksum(state: *const GameState) u32 {
|
|
var h = std.hash.Fnv1a_32.init();
|
|
h.update(std.mem.asBytes(&state.tick));
|
|
h.update(std.mem.asBytes(&state.players));
|
|
h.update(std.mem.asBytes(&state.wind.raw));
|
|
if (state.projectile) |p| h.update(std.mem.asBytes(&p));
|
|
return h.final();
|
|
}
|
|
```
|
|
|
|
## common fixed-point constants
|
|
|
|
```zig
|
|
pub const ZERO = Fixed{ .raw = 0 };
|
|
pub const ONE = Fixed{ .raw = 1 << 32 };
|
|
pub const HALF = Fixed{ .raw = 1 << 31 };
|
|
pub const TWO = Fixed{ .raw = 2 << 32 };
|
|
pub const PI = Fixed{ .raw = 13493037705 };
|
|
pub const TWO_PI = Fixed{ .raw = 26986075409 };
|
|
pub const HALF_PI = Fixed{ .raw = 6746518852 };
|
|
|
|
// game constants
|
|
pub const GRAVITY = Fixed{ .raw = 42949673 }; // ~0.01
|
|
pub const WIND_FACTOR = Fixed{ .raw = 4294967 }; // ~0.001
|
|
pub const MAX_POWER = Fixed{ .raw = 100 << 32 };
|
|
```
|