Files
sinbad/src/appGameIslands.c
T

200 lines
8.0 KiB
C

#include "islands.h"
#include "engineCamera.h"
#include "enginePhysics.h"
#include "engineRectangle.h"
#include "engineVector.h"
#include <math.h>
#include <raylib.h>
#include <raymath.h>
// How far an island can drift from the camera before it's considered "left
// behind" and gets regenerated fresh outside the current view, the same
// recycling idea used for enemies/birds so the world stays populated as the
// player explores.
#define ISLAND_RECYCLE_DISTANCE 800.0f
#define ISLAND_MIN_SPAWN_DISTANCE 80
#define ISLAND_MAX_SPAWN_DISTANCE 600
// Concentric elevation bands, each a fraction of the coastline radius at
// that angle — same idea as a real elevation-banded map (shore/lowland/
// highland rings), just three tiers instead of five since this renders at
// pixel-art scale, not map scale.
#define ISLAND_GRASS_SCALE 0.72f
#define ISLAND_HILL_SCALE 0.35f
// Native resolution the shape is painted at. Higher than the 16x16 ship/bird
// sprites — islands are the biggest thing on screen now, and matching their
// texel density would make each "pixel" a huge, coarse block; this keeps
// the pixel-art look while the individual pixels stay small relative to the
// island's size.
#define ISLAND_IMAGE_SIZE 128
// A coastline is two overlapping cosine lobes (a broad one for bays/
// peninsulas, a finer one for coastal roughness) plus light jitter, instead
// of a single jittered circle — the same kind of shape irregularity a real
// procedural terrain generator uses, just far cheaper.
static void GenerateCoastline(float shoreRadiusPx[ISLAND_SHAPE_POINTS], float baseRadius) {
float amp1 = (float)GetRandomValue(15, 30) / 100.0f;
float freq1 = (float)GetRandomValue(2, 3);
float phase1 = (float)GetRandomValue(0, 628) / 100.0f;
float amp2 = (float)GetRandomValue(5, 15) / 100.0f;
float freq2 = (float)GetRandomValue(5, 8);
float phase2 = (float)GetRandomValue(0, 628) / 100.0f;
for (int i = 0; i < ISLAND_SHAPE_POINTS; i++) {
float angle = (2.0f * PI * (float)i) / (float)ISLAND_SHAPE_POINTS;
float shapeFactor = 1.0f
+ amp1 * cosf(freq1 * angle + phase1)
+ amp2 * cosf(freq2 * angle + phase2);
if (shapeFactor < 0.5f) shapeFactor = 0.5f;
float jitter = (float)GetRandomValue(90, 110) / 100.0f;
shoreRadiusPx[i] = baseRadius * shapeFactor * jitter;
}
// Average each radius with its neighbours a couple of times so the
// outline reads as an irregular but rounded coastline instead of a spiky
// star.
for (int pass = 0; pass < 2; pass++) {
float smoothed[ISLAND_SHAPE_POINTS];
for (int i = 0; i < ISLAND_SHAPE_POINTS; i++) {
int prev = (i - 1 + ISLAND_SHAPE_POINTS) % ISLAND_SHAPE_POINTS;
int next = (i + 1) % ISLAND_SHAPE_POINTS;
smoothed[i] = (shoreRadiusPx[prev] + shoreRadiusPx[i] + shoreRadiusPx[next]) / 3.0f;
}
for (int i = 0; i < ISLAND_SHAPE_POINTS; i++) shoreRadiusPx[i] = smoothed[i];
}
}
static float SampleRadius(const float shoreRadiusPx[ISLAND_SHAPE_POINTS], float angle) {
float t = angle / (2.0f * PI) * ISLAND_SHAPE_POINTS;
int i0 = ((int)t) % ISLAND_SHAPE_POINTS;
int i1 = (i0 + 1) % ISLAND_SHAPE_POINTS;
float frac = t - (int)t;
return shoreRadiusPx[i0] * (1.0f - frac) + shoreRadiusPx[i1] * frac;
}
static Texture2D GenerateIslandTexture(const float shoreRadiusPx[ISLAND_SHAPE_POINTS],
Color shoreColor, Color grassColor, Color hillColor) {
Image image = GenImageColor(ISLAND_IMAGE_SIZE, ISLAND_IMAGE_SIZE, BLANK);
float center = ISLAND_IMAGE_SIZE / 2.0f;
for (int y = 0; y < ISLAND_IMAGE_SIZE; y++) {
for (int x = 0; x < ISLAND_IMAGE_SIZE; x++) {
float dx = (x + 0.5f) - center;
float dy = (y + 0.5f) - center;
float dist = sqrtf(dx * dx + dy * dy);
float angle = atan2f(dy, dx);
if (angle < 0.0f) angle += 2.0f * PI;
float shoreRadius = SampleRadius(shoreRadiusPx, angle);
if (dist <= shoreRadius * ISLAND_HILL_SCALE) {
ImageDrawPixel(&image, x, y, hillColor);
} else if (dist <= shoreRadius * ISLAND_GRASS_SCALE) {
ImageDrawPixel(&image, x, y, grassColor);
} else if (dist <= shoreRadius) {
ImageDrawPixel(&image, x, y, shoreColor);
}
}
}
Texture2D texture = LoadTextureFromImage(image);
UnloadImage(image);
SetTextureFilter(texture, TEXTURE_FILTER_POINT);
return texture;
}
static void GenerateIsland(Island *island, Vector2 position) {
if (island->sprite.texture.id != 0) {
UnloadTexture(island->sprite.texture);
}
island->position = position;
float worldDiameter = (float)GetRandomValue(220, 380);
float basePixelRadius = (float)GetRandomValue(30, 44);
float shoreRadiusPx[ISLAND_SHAPE_POINTS];
GenerateCoastline(shoreRadiusPx, basePixelRadius);
// The collision radius has to track the coastline's actual average
// extent, not a fraction of the destRec/canvas size — the canvas has a
// fair amount of open water margin around the shore (needed so the
// harmonic wobble and jitter don't clip off-canvas), so anything based on
// worldDiameter directly ends up much bigger than the visible island,
// making the ship "bump" into open water well before the shore.
float avgRadiusPx = 0.0f;
for (int i = 0; i < ISLAND_SHAPE_POINTS; i++) avgRadiusPx += shoreRadiusPx[i];
avgRadiusPx /= ISLAND_SHAPE_POINTS;
float pixelToWorld = worldDiameter / (float)ISLAND_IMAGE_SIZE;
// Slightly over the average so the collision circle covers the coastline
// at most angles instead of sitting exactly on it — the shape bulges out
// past the average at peninsulas, and a ship visually overlapping the
// shore reads worse than the collision boundary having a little slack.
island->radius = avgRadiusPx * pixelToWorld * 1.05f;
// Slight per-island tint variation so a cluster of islands doesn't look
// like the same stamp copy-pasted around.
Color shoreColor = (Color){
(unsigned char)Clamp(233 + GetRandomValue(-10, 10), 0, 255),
(unsigned char)Clamp(220 + GetRandomValue(-10, 10), 0, 255),
(unsigned char)Clamp(178 + GetRandomValue(-10, 10), 0, 255),
255};
Color grassColor = (Color){
(unsigned char)Clamp(80 + GetRandomValue(-15, 15), 0, 255),
(unsigned char)Clamp(168 + GetRandomValue(-15, 15), 0, 255),
(unsigned char)Clamp(84 + GetRandomValue(-15, 15), 0, 255),
255};
Color hillColor = (Color){
(unsigned char)Clamp(52 + GetRandomValue(-12, 12), 0, 255),
(unsigned char)Clamp(120 + GetRandomValue(-12, 15), 0, 255),
(unsigned char)Clamp(64 + GetRandomValue(-12, 12), 0, 255),
255};
Texture2D texture = GenerateIslandTexture(shoreRadiusPx, shoreColor, grassColor, hillColor);
island->sprite = (Sprite){
.texture = texture,
.frameSize = {ISLAND_IMAGE_SIZE, ISLAND_IMAGE_SIZE},
.sourceRec = {0.0f, 0.0f, ISLAND_IMAGE_SIZE, ISLAND_IMAGE_SIZE},
.destRec = {position.x, position.y, worldDiameter, worldDiameter},
.origin = {worldDiameter / 2.0f, worldDiameter / 2.0f},
.rotation = 0.0f,
.color = WHITE};
}
Islands CreateIslands(Camera2D *camera) {
Islands islands = {.count = MAX_ISLANDS};
Rectangle cameraRec = GetCameraRectangle(camera);
for (int i = 0; i < islands.count; i++) {
GenerateIsland(&islands.island[i],
GetPositionOutsideRectangle(cameraRec, ISLAND_MIN_SPAWN_DISTANCE, ISLAND_MAX_SPAWN_DISTANCE));
}
return islands;
}
void UpdateIslands(Islands *islands, Camera2D *camera) {
Rectangle cameraRec = GetCameraRectangle(camera);
for (int i = 0; i < islands->count; i++) {
if (GetDistanceBetweenTwoVectors(islands->island[i].position, camera->target) > ISLAND_RECYCLE_DISTANCE) {
GenerateIsland(&islands->island[i],
GetPositionOutsideRectangle(cameraRec, ISLAND_MIN_SPAWN_DISTANCE, ISLAND_MAX_SPAWN_DISTANCE));
}
}
}
void RenderIslands(Islands *islands) {
for (int i = 0; i < islands->count; i++) {
RenderSprite(&islands->island[i].sprite);
}
}
void UnloadIslands(Islands *islands) {
for (int i = 0; i < islands->count; i++) {
UnloadTexture(islands->island[i].sprite.texture);
}
}