File size: 10,635 Bytes
3ce1a40 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 | // Generates the demo game's binary assets (original pixel art, SFX, streamed music) so
// the repository ships no third-party media. Deterministic: same output every run.
// unify_assetgen <assets-dir>
#include "render/render.h"
#include "audio/audio.h"
#include <cmath>
#include <cstdio>
#include <string>
#include <vector>
#include <filesystem>
using namespace unify;
static void px(Image& im, int x, int y, Color c) {
if (x < 0 || y < 0 || x >= im.width || y >= im.height) return;
uint8_t* p = im.at(x, y);
p[0] = c.r; p[1] = c.g; p[2] = c.b; p[3] = c.a;
}
static void rect(Image& im, int x, int y, int w, int h, Color c) { for (int j = 0; j < h; j++) for (int i = 0; i < w; i++) px(im, x + i, y + j, c); }
static void disc(Image& im, float cx, float cy, float r, Color c) {
for (int y = 0; y < im.height; y++) for (int x = 0; x < im.width; x++)
if ((x + 0.5f - cx) * (x + 0.5f - cx) + (y + 0.5f - cy) * (y + 0.5f - cy) <= r * r) px(im, x, y, c);
}
// Player: 16x24, a small robot courier. `pose`: 0/1 idle breathe, 2..5 run cycle, 6 jump, 7 fall.
static Image player(int pose) {
Image im(16, 24);
const Color body{64, 156, 255, 255}, dark{30, 70, 150, 255}, visor{240, 250, 255, 255}, eye{20, 20, 40, 255}, boot{40, 40, 52, 255};
int bob = pose == 1 ? 1 : 0;
rect(im, 3, 3 + bob, 10, 9, body); // head
rect(im, 4, 5 + bob, 8, 4, visor); // visor
rect(im, 6, 6 + bob, 1, 2, eye);
rect(im, 9, 6 + bob, 1, 2, eye);
rect(im, 7, 1 + bob, 2, 2, dark); // antenna
rect(im, 4, 12 + bob, 8, 7, body); // torso
rect(im, 5, 14 + bob, 6, 1, dark);
int lf = 0, rf = 0; // leg offsets
if (pose >= 2 && pose <= 5) { static const int cyc[4][2] = {{-2, 2}, {0, 0}, {2, -2}, {0, 0}}; lf = cyc[pose - 2][0]; rf = cyc[pose - 2][1]; }
if (pose == 6) { lf = -1; rf = 1; }
rect(im, 5 + lf, 19, 2, 4, dark);
rect(im, 9 + rf, 19, 2, 4, dark);
rect(im, 4 + lf, 22, 3, 2, boot);
rect(im, 9 + rf, 22, 3, 2, boot);
if (pose == 6) { rect(im, 1, 9, 3, 2, body); rect(im, 12, 9, 3, 2, body); } // arms up
else if (pose == 7) { rect(im, 1, 13, 3, 2, body); rect(im, 12, 13, 3, 2, body); } // arms out
else { rect(im, 2, 13 + bob, 2, 5, body); rect(im, 12, 13 + bob, 2, 5, body); }
return im;
}
static Image tile(bool top) {
Image im(16, 16, {120, 84, 52, 255});
for (int i = 0; i < 16; i++) for (int j = 0; j < 16; j++) if (((i * 7 + j * 13) % 11) == 0) px(im, i, j, {98, 66, 40, 255});
if (top) { rect(im, 0, 0, 16, 4, {86, 190, 90, 255}); for (int i = 0; i < 16; i += 3) px(im, i, 4, {86, 190, 90, 255}); rect(im, 0, 0, 16, 1, {140, 230, 130, 255}); }
return im;
}
static Image crate() {
Image im(16, 16, {176, 122, 60, 255});
rect(im, 0, 0, 16, 2, {120, 80, 36, 255}); rect(im, 0, 14, 16, 2, {120, 80, 36, 255});
rect(im, 0, 0, 2, 16, {120, 80, 36, 255}); rect(im, 14, 0, 2, 16, {120, 80, 36, 255});
for (int i = 2; i < 14; i++) { px(im, i, i, {120, 80, 36, 255}); px(im, i, 15 - i, {120, 80, 36, 255}); }
return im;
}
static Image coin(int f) {
Image im(12, 12);
float w = std::fabs(std::cos(f * 3.14159f / 4)) * 5 + 1;
for (int y = 0; y < 12; y++) for (int x = 0; x < 12; x++) {
float dx = (x + 0.5f - 6) / w, dy = (y + 0.5f - 6) / 5.5f;
if (dx * dx + dy * dy <= 1) px(im, x, y, dx * dx + dy * dy < 0.4f ? Color{255, 236, 120, 255} : Color{232, 176, 40, 255});
}
return im;
}
static Image ball() { Image im(16, 16); disc(im, 8, 8, 7.5f, {230, 70, 90, 255}); disc(im, 6, 5, 2.5f, {255, 180, 190, 255}); return im; }
static Image spark() { Image im(4, 4); rect(im, 1, 0, 2, 4, {255, 250, 200, 255}); rect(im, 0, 1, 4, 2, {255, 250, 200, 255}); return im; }
static Image flag(int f) {
Image im(16, 32);
rect(im, 2, 0, 2, 32, {220, 220, 230, 255});
for (int y = 0; y < 10; y++) for (int x = 0; x < 10; x++) {
int wave = int(std::round(std::sin((x + f * 2) * 0.8) * 1.2));
px(im, 4 + x, 2 + y + wave, (x / 2 + y / 2) % 2 ? Color{40, 40, 40, 255} : Color{250, 250, 250, 255});
}
return im;
}
static Image platform() {
Image im(48, 12, {90, 100, 128, 255});
rect(im, 0, 0, 48, 2, {150, 170, 210, 255});
for (int x = 2; x < 48; x += 8) rect(im, x, 5, 4, 3, {60, 66, 90, 255});
return im;
}
static Image sky() {
Image im(16, 64);
for (int y = 0; y < 64; y++) { float t = y / 63.0f; rect(im, 0, y, 16, 1, {uint8_t(40 + 50 * t), uint8_t(60 + 90 * t), uint8_t(120 + 100 * t), 255}); }
return im;
}
static Image cloud() {
Image im(32, 16);
disc(im, 9, 10, 6, {245, 248, 255, 255}); disc(im, 17, 7, 7, {245, 248, 255, 255}); disc(im, 24, 10, 6, {245, 248, 255, 255});
rect(im, 8, 10, 18, 6, {245, 248, 255, 255});
return im;
}
// ---------------------------------------------------------------------------------- audio
static std::vector<int16_t> render_sfx(float seconds, float (*fn)(float t, float dur)) {
int n = int(seconds * 44100);
std::vector<int16_t> pcm(size_t(n) * 2);
for (int i = 0; i < n; i++) {
float v = fn(i / 44100.0f, seconds);
int16_t s = int16_t(std::lround(std::max(-1.0f, std::min(1.0f, v)) * 20000));
pcm[size_t(i) * 2] = pcm[size_t(i) * 2 + 1] = s;
}
return pcm;
}
static float square(float phase) { return std::fmod(phase, 1.0f) < 0.5f ? 1.0f : -1.0f; }
static std::vector<int16_t> render_music(int* loop_start, int* loop_end) {
// 8 bars of a looping chiptune at 120 BPM (16 s). Loop points skip the 2-bar intro.
const float bpm = 120, beat = 60 / bpm;
const int bars = 10, total = int(bars * 4 * beat * 44100);
std::vector<float> l(size_t(total), 0), r(size_t(total), 0);
static const int bass[8] = {45, 45, 41, 41, 43, 43, 40, 40};
static const int melody[32] = {69, 72, 76, 72, 74, 72, 69, 67, 65, 69, 72, 69, 67, 71, 74, 71,
69, 72, 76, 79, 77, 76, 74, 72, 71, 72, 74, 76, 72, 71, 69, -1};
auto note_freq = [](int n) { return 440.0f * std::pow(2.0f, (n - 69) / 12.0f); };
for (int i = 0; i < total; i++) {
float t = i / 44100.0f;
int step = int(t / (beat / 2)); // eighth notes
int bar = int(t / (4 * beat));
float in_step = std::fmod(t, beat / 2) / (beat / 2);
float env = std::exp(-in_step * 3);
float b = square(t * note_freq(bass[(bar * 2 + (step % 8) / 4) % 8] - 12)) * 0.18f * (0.6f + 0.4f * env);
float m = 0;
if (bar >= 2) {
int n = melody[step % 32];
if (n > 0) m = square(t * note_freq(n)) * 0.12f * env + square(t * note_freq(n) * 1.005f) * 0.05f * env;
}
float hat = (step % 2 == 1 && in_step < 0.08f) ? (float((i * 1103515245u + 12345u) >> 16 & 0xFF) / 128.0f - 1) * 0.06f * (1 - in_step / 0.08f) : 0;
l[size_t(i)] = b + m * 0.8f + hat;
r[size_t(i)] = b + m + hat * 0.7f;
}
std::vector<int16_t> pcm(size_t(total) * 2);
for (int i = 0; i < total; i++) {
pcm[size_t(i) * 2] = int16_t(std::lround(std::max(-1.0f, std::min(1.0f, l[size_t(i)])) * 26000));
pcm[size_t(i) * 2 + 1] = int16_t(std::lround(std::max(-1.0f, std::min(1.0f, r[size_t(i)])) * 26000));
}
*loop_start = int(2 * 4 * beat * 44100);
*loop_end = total;
return pcm;
}
int main(int argc, char** argv) {
if (argc < 2) { std::fprintf(stderr, "usage: unify_assetgen <assets-dir>\n"); return 2; }
std::string root = argv[1];
for (const char* d : {"textures", "audio", "music", "saves"}) std::filesystem::create_directories(root + "/" + d);
std::vector<std::pair<std::string, Image>> sprites;
const char* poses[8] = {"player_idle_0", "player_idle_1", "player_run_0", "player_run_1", "player_run_2", "player_run_3", "player_jump", "player_fall"};
for (int i = 0; i < 8; i++) sprites.push_back({poses[i], player(i)});
sprites.push_back({"tile_top", tile(true)});
sprites.push_back({"tile", tile(false)});
sprites.push_back({"crate", crate()});
for (int i = 0; i < 4; i++) sprites.push_back({"coin_" + std::to_string(i), coin(i)});
sprites.push_back({"ball", ball()});
sprites.push_back({"spark", spark()});
sprites.push_back({"flag_0", flag(0)});
sprites.push_back({"flag_1", flag(1)});
sprites.push_back({"platform", platform()});
sprites.push_back({"sky", sky()});
sprites.push_back({"cloud", cloud()});
std::vector<std::pair<std::string, const Image*>> items;
for (auto& s : sprites) items.push_back({s.first, &s.second});
Image atlas_img;
Atlas atlas;
if (!pack_atlas(items, 128, atlas_img, atlas)) { std::fprintf(stderr, "atlas too small\n"); return 1; }
auto qoi = encode_qoi(atlas_img);
FILE* f = std::fopen((root + "/textures/sprites.qoi").c_str(), "wb");
std::fwrite(qoi.data(), 1, qoi.size(), f);
std::fclose(f);
f = std::fopen((root + "/textures/sprites.atlas").c_str(), "w");
for (size_t i = 1; i < atlas.regions.size(); i++) {
const auto& r = atlas.regions[i];
std::fprintf(f, "%s %d %d %d %d\n", r.name.c_str(), r.rect.x, r.rect.y, r.rect.w, r.rect.h);
}
std::fclose(f);
write_wav(root + "/audio/jump.wav", render_sfx(0.18f, [](float t, float d) { return square(t * (300 + 900 * t / d)) * 0.5f * (1 - t / d); }), 2, 44100);
write_wav(root + "/audio/coin.wav", render_sfx(0.25f, [](float t, float d) { return square(t * (t < 0.07f ? 988.0f : 1319.0f)) * 0.4f * (1 - t / d); }), 2, 44100);
write_wav(root + "/audio/land.wav", render_sfx(0.12f, [](float t, float d) {
float noise = float((uint32_t(t * 44100) * 2654435761u) >> 20 & 0xFFF) / 2048.0f - 1;
return noise * 0.35f * (1 - t / d) + std::sin(t * 2 * 3.14159f * 90) * 0.4f * (1 - t / d); }), 2, 44100);
write_wav(root + "/audio/bounce.wav", render_sfx(0.15f, [](float t, float d) { return std::sin(t * 2 * 3.14159f * (220 + 440 * (1 - t / d))) * 0.6f * (1 - t / d); }), 2, 44100);
write_wav(root + "/audio/win.wav", render_sfx(0.9f, [](float t, float d) {
static const float notes[] = {523.25f, 659.25f, 783.99f, 1046.5f};
int i = std::min(3, int(t / 0.15f));
return square(t * notes[i]) * 0.35f * (i == 3 ? 1 - (t - 0.45f) / (d - 0.45f) : 1.0f); }), 2, 44100);
int ls, le;
auto music = render_music(&ls, &le);
write_wav(root + "/music/theme.wav", music, 2, 44100, ls, le);
std::printf("assets written to %s (%zu sprites, atlas %dx%d, music %.1fs)\n", root.c_str(), sprites.size(), atlas_img.width, atlas_img.height, music.size() / 2 / 44100.0);
return 0;
}
|