Big change to make resources easier to use.

This commit is contained in:
Kapendev 2024-08-28 03:51:33 +03:00
parent 8ef32abe76
commit 7a801a02bc
12 changed files with 350 additions and 232 deletions

View file

@ -10,7 +10,7 @@ void ready() {
lockResolution(320, 180);
}
bool update() {
bool update(float dt) {
drawDebugText("Hello world!");
return false;
}

View file

@ -10,17 +10,17 @@ void ready() {
lockResolution(320, 180);
}
bool update() {
bool update(float dt) {
// Move the camera.
cameraTarget += wasd * cameraSpeed * Vec2(deltaTime);
camera.followPosition(cameraTarget, Vec2(deltaTime));
cameraTarget += wasd * cameraSpeed * Vec2(dt);
camera.followPosition(cameraTarget, Vec2(dt));
// Draw the game world.
auto cameraArea = Rect(camera.position, resolution).area(camera.hook).subAll(3);
attachCamera(camera);
camera.attach();
drawDebugText("Move with arrow keys.");
drawRect(cameraArea, Color(50, 50, 40, 130));
detachCamera(camera);
camera.detach();
// Draw the game UI.
drawDebugText("I am UI!");

View file

@ -4,7 +4,7 @@ import popka;
// The game variables.
auto player = Rect(16, 16);
auto playerSpeed = Vec2(120);
auto coins = FlagList!Rect();
auto coins = SparseList!Rect();
auto coinSize = Vec2(8);
auto maxCoinCount = 8;
@ -24,7 +24,7 @@ void ready() {
}
}
bool update() {
bool update(float dt) {
// Move the player.
auto playerDirection = Vec2();
if (Keyboard.left.isDown || 'a'.isDown) {
@ -39,7 +39,7 @@ bool update() {
if (Keyboard.down.isDown || 's'.isDown) {
playerDirection.y = 1;
}
player.position += playerDirection * playerSpeed * Vec2(deltaTime);
player.position += playerDirection * playerSpeed * Vec2(dt);
// Check if the player is touching some coins and remove those coins.
foreach (id; coins.ids) {
@ -62,8 +62,6 @@ bool update() {
return false;
}
void finish() {
coins.free();
}
void finish() { }
mixin runGame!(ready, update, finish);

View file

@ -40,7 +40,7 @@ void ready() {
dialogue.update();
}
bool update() {
bool update(float dt) {
// Update the game.
if (dialogue.canUpdate) {
if (dialogue.hasChoices) {
@ -77,8 +77,6 @@ bool update() {
return false;
}
void finish() {
dialogue.free();
}
void finish() { }
mixin runGame!(ready, update, finish);

View file

@ -2,7 +2,7 @@
import popka;
// The game variables.
auto atlas = Texture();
auto atlas = TextureId();
auto frame = 0.0;
auto frameCount = 2;
auto frameSpeed = 8;
@ -20,10 +20,10 @@ void ready() {
atlas = loadTexture("atlas.png").unwrap();
}
bool update() {
bool update(float dt) {
// Move the frame around in a smooth way and update the current frame.
framePosition = framePosition.moveToWithSlowdown(mouseScreenPosition, Vec2(deltaTime), frameSlowdown);
frame = wrap(frame + deltaTime * frameSpeed, 0, frameCount);
framePosition = framePosition.moveToWithSlowdown(mouseScreenPosition, Vec2(dt), frameSlowdown);
frame = wrap(frame + dt * frameSpeed, 0, frameCount);
// Check the mouse move direction and make the sprite look at that direction.
auto mouseDirection = framePosition.directionTo(mouseScreenPosition);
@ -42,13 +42,11 @@ bool update() {
options.scale = Vec2(2);
// Draw the frame and the mouse position.
drawTexture(atlas, framePosition, Rect(frameSize.x * floor(frame), 128, frameSize), options);
drawTexture(atlas.get(), framePosition, Rect(frameSize.x * floor(frame), 128, frameSize), options);
drawVec2(mouseScreenPosition, 8, frame == 0 ? blank : white.alpha(150));
return false;
}
void finish() {
atlas.free();
}
void finish() { }
mixin runGame!(ready, update, finish);

View file

@ -8,7 +8,7 @@ void ready() {
// The update function. This is called every frame while the game is running.
// If true is returned, then the game will stop running.
bool update() {
bool update(float dt) {
drawDebugText("Hello world!");
return false;
}

View file

@ -2,7 +2,7 @@
import popka;
// The game variables.
auto atlas = Texture();
auto atlas = TextureId();
auto map = TileMap();
void ready() {
@ -12,16 +12,13 @@ void ready() {
map.parse("145,0,65\n21,22,23\n37,38,39\n53,54,55", 16, 16);
}
bool update() {
bool update(float dt) {
auto options = DrawOptions();
options.scale = Vec2(2.0f);
drawTileMap(atlas, Vec2(), map, Camera(), options);
drawTileMap(atlas.get(), Vec2(), map, Camera(), options);
return false;
}
void finish() {
atlas.free();
map.free();
}
void finish() { }
mixin runGame!(ready, update, finish);

View file

@ -18,7 +18,7 @@ void ready() {
paddle2.position = resolution * Vec2(0.5) + paddleOffset;
}
bool update() {
bool update(float dt) {
// The objects in this game are centered.
// To do that, we split the rectangle data into 2 parts, normal and centered.
// A normal rectangle holds the real position of an object.
@ -43,16 +43,16 @@ bool update() {
ballDirection.y *= -1;
ball.position.y = resolution.y - ball.size.y * 0.5;
}
ball.position += ballDirection * ballSpeed * Vec2(deltaTime);
ball.position += ballDirection * ballSpeed * Vec2(dt);
// Move paddle1.
paddle1.position.y = clamp(paddle1.position.y + wasd.y * ballSpeed.y * deltaTime, paddle1.size.y * 0.5, resolution.y - paddle1.size.y * 0.5);
paddle1.position.y = clamp(paddle1.position.y + wasd.y * ballSpeed.y * dt, paddle1.size.y * 0.5, resolution.y - paddle1.size.y * 0.5);
// Move paddle2.
auto paddle2Target = ball.position.y;
if (ballDirection.x < 1) {
paddle2Target = paddle2.position.y;
}
paddle2.position.y = paddle2.position.y.moveTo(clamp(paddle2Target, paddle2.size.y * 0.5f, resolution.y - paddle2.size.y * 0.5f), ballSpeed.y * deltaTime);
paddle2.position.y = paddle2.position.y.moveTo(clamp(paddle2Target, paddle2.size.y * 0.5f, resolution.y - paddle2.size.y * 0.5f), ballSpeed.y * dt);
// Check for collisions.
if (paddle1.centerArea.hasIntersection(ball.centerArea)) {

View file

@ -343,7 +343,7 @@ Result!Dialogue toDialogue(IStr script) {
return Result!Dialogue(value, fault);
}
Result!Dialogue loadDialogue(IStr path) {
Result!Dialogue loadRawDialogue(IStr path) {
auto temp = loadTempText(path);
if (temp.isNone) {
return Result!Dialogue(temp.fault);

View file

@ -10,6 +10,7 @@
// TODO: Think about toggle functions.
// TODO: Update setup script.
// TODO: Clean web script.
// TODO: Make the locked resolution image more pixel perfect friendly by maybe hiding some pixels when the window resolution is weird.
/// The `engine` module functions as a lightweight 2D game engine.
module popka.engine;
@ -189,6 +190,60 @@ struct Camera {
scale = scale.moveToWithSlowdown(target, delta, slowdown);
}
}
@trusted
void attach() {
if (isAttached) {
return;
}
isAttached = true;
auto temp = this.toRl();
if (isPixelPerfect) {
temp.target.x = floor(temp.target.x);
temp.target.y = floor(temp.target.y);
temp.offset.x = floor(temp.offset.x);
temp.offset.y = floor(temp.offset.y);
}
rl.BeginMode2D(temp);
}
@trusted
void detach() {
if (isAttached) {
isAttached = false;
rl.EndMode2D();
}
}
}
struct TextId {
GenerationalIndex data;
alias data this;
@safe @nogc nothrow:
bool isValid() {
return data.value != 0 && engineState.resources.texts.has(data);
}
ref LStr get() {
if (!isValid) {
assert(0, "Index `{}` with generation `{}` does not exist.".format(data.value, data.generation));
}
return engineState.resources.texts[data];
}
LStr getOr() {
return isValid ? get() : LStr();
}
void free() {
if (engineState.resources.texts.has(data)) {
engineState.resources.texts[data].free();
engineState.resources.texts.remove(data);
engineState.resources.textNames.remove(data);
}
}
}
struct Texture {
@ -237,8 +292,35 @@ struct Texture {
}
}
// Note: Think about adding a tag to the resource ids. A tag could be used to free only some resources.
struct TextureId {
GenerationalIndex data;
alias data this;
@safe @nogc nothrow:
bool isValid() {
return data.value != 0 && engineState.resources.textures.has(data);
}
ref Texture get() {
if (!isValid) {
assert(0, "Index `{}` with generation `{}` does not exist.".format(data.value, data.generation));
}
return engineState.resources.textures[data];
}
Texture getOr() {
return isValid ? get() : Texture();
}
void free() {
if (engineState.resources.textures.has(data)) {
engineState.resources.textures[data].free();
engineState.resources.textures.remove(data);
engineState.resources.textureNames.remove(data);
}
}
}
struct Font {
@ -279,38 +361,52 @@ struct Font {
struct FontId {
GenerationalIndex data;
}
struct Audio {
Data data;
alias Sound = rl.Sound;
alias Music = rl.Music;
alias Data = Variant!(Sound, Music);
alias data this;
@safe @nogc nothrow:
Sound sound() {
return data.get!Sound();
bool isValid() {
return data.value != 0 && engineState.resources.fonts.has(data);
}
Music music() {
return data.get!Music();
ref Font get() {
if (!isValid) {
assert(0, "Index `{}` with generation `{}` does not exist.".format(data.value, data.generation));
}
return engineState.resources.fonts[data];
}
Font getOr() {
return isValid ? get() : Font();
}
void free() {
if (engineState.resources.fonts.has(data)) {
engineState.resources.fonts[data].free();
engineState.resources.fonts.remove(data);
engineState.resources.fontNames.remove(data);
}
}
}
struct Sound {
Variant!(rl.Sound, rl.Music) data;
@safe @nogc nothrow:
bool isSound() {
return data.isKind!Sound;
return data.isKind!(rl.Sound);
}
bool isMusic() {
return data.isKind!Music;
return data.isKind!(rl.Music);
}
bool isEmpty() {
if (isSound) {
return sound.stream.sampleRate == 0;
return data.get!(rl.Sound)().stream.sampleRate == 0;
} else {
return music.stream.sampleRate == 0;
return data.get!(rl.Music)().stream.sampleRate == 0;
}
}
@ -319,7 +415,7 @@ struct Audio {
if (isSound) {
return 0.0f;
} else {
return rl.GetMusicTimePlayed(music);
return rl.GetMusicTimePlayed(data.get!(rl.Music)());
}
}
@ -328,34 +424,34 @@ struct Audio {
if (isSound) {
return 0.0f;
} else {
return rl.GetMusicTimeLength(music);
return rl.GetMusicTimeLength(data.get!(rl.Music)());
}
}
@trusted
void setVolume(float value) {
if (isSound) {
rl.SetSoundVolume(sound, value);
rl.SetSoundVolume(data.get!(rl.Sound)(), value);
} else {
rl.SetMusicVolume(music, value);
rl.SetMusicVolume(data.get!(rl.Music)(), value);
}
}
@trusted
void setPitch(float value) {
if (isSound) {
rl.SetSoundPitch(sound, value);
rl.SetSoundPitch(data.get!(rl.Sound)(), value);
} else {
rl.SetMusicPitch(music, value);
rl.SetMusicPitch(data.get!(rl.Music)(), value);
}
}
@trusted
void setPan(float value) {
if (isSound) {
rl.SetSoundPan(sound, value);
rl.SetSoundPan(data.get!(rl.Sound)(), value);
} else {
rl.SetMusicPan(music, value);
rl.SetMusicPan(data.get!(rl.Music)(), value);
}
}
@ -365,16 +461,42 @@ struct Audio {
return;
}
if (isSound) {
rl.UnloadSound(sound);
rl.UnloadSound(data.get!(rl.Sound)());
} else {
rl.UnloadMusicStream(music);
rl.UnloadMusicStream(data.get!(rl.Music)());
}
this = Audio();
this = Sound();
}
}
struct AudioId {
struct SoundId {
GenerationalIndex data;
alias data this;
@safe @nogc nothrow:
bool isValid() {
return data.value != 0 && engineState.resources.sounds.has(data);
}
ref Sound get() {
if (!isValid) {
assert(0, "Index `{}` with generation `{}` does not exist.".format(data.value, data.generation));
}
return engineState.resources.sounds[data];
}
Sound getOr() {
return isValid ? get() : Sound();
}
void free() {
if (engineState.resources.sounds.has(data)) {
engineState.resources.sounds[data].free();
engineState.resources.sounds.remove(data);
engineState.resources.soundNames.remove(data);
}
}
}
struct Viewport {
@ -424,37 +546,60 @@ struct Viewport {
struct EngineFlags {
bool isUpdating;
bool isPixelPerfect;
bool isVsync;
bool isFpsLocked;
bool isCursorHidden;
}
struct EngineResources {
GenerationalList!LStr textNames;
GenerationalList!LStr texts;
GenerationalList!LStr textureNames;
GenerationalList!Texture textures;
GenerationalList!LStr fontNames;
GenerationalList!Font fonts;
GenerationalList!Audio audios;
GenerationalList!LStr soundNames;
GenerationalList!Sound sounds;
@safe @nogc nothrow:
void free() {
debug println("Ending cleanup:");
debug printfln(" Texture count freed: {}", textures.length);
foreach (name; textNames.items) {
name.free();
}
textNames.free();
foreach (ref text; texts.items) {
text.free();
}
texts.free();
foreach (name; textureNames.items) {
name.free();
}
textureNames.free();
foreach (ref texture; textures.items) {
texture.free();
}
textures.free();
debug printfln(" Font count freed: {}", fonts.length);
foreach (name; fontNames.items) {
name.free();
}
fontNames.free();
foreach (ref font; fonts.items) {
font.free();
}
fonts.free();
debug printfln(" Audio count freed: {}", audios.length);
foreach (ref audio; audios.items) {
audio.free();
foreach (name; soundNames.items) {
name.free();
}
audios.free();
soundNames.free();
foreach (ref sound; sounds.items) {
sound.free();
}
sounds.free();
}
}
@ -483,6 +628,7 @@ struct EngineState {
Color backgroundColor;
LStr tempText;
LStr assetsPath;
ulong tickCount;
@safe @nogc nothrow:
@ -598,63 +744,6 @@ rl.Camera2D toRl(Camera camera) {
);
}
bool hasResource(TextureId id) {
return engineState.resources.textures.has(id.data);
}
bool hasResource(FontId id) {
return engineState.resources.fonts.has(id.data);
}
bool hasResource(AudioId id) {
return engineState.resources.audios.has(id.data);
}
ref Texture getResource(TextureId id) {
return engineState.resources.textures[id.data];
}
ref Font getResource(FontId id) {
return engineState.resources.fonts[id.data];
}
ref Audio getResource(AudioId id) {
return engineState.resources.audios[id.data];
}
Texture getResourceOr(TextureId id) {
return hasResource(id) ? getResource(id) : Texture();
}
Font getResourceOr(FontId id) {
return hasResource(id) ? getResource(id) : Font();
}
Audio getResourceOr(AudioId id) {
return hasResource(id) ? getResource(id) : Audio();
}
void unloadResource(TextureId id) {
if (engineState.resources.textures.has(id.data)) {
engineState.resources.textures[id.data].free();
engineState.resources.textures.remove(id.data);
}
}
void unloadResource(FontId id) {
if (engineState.resources.fonts.has(id.data)) {
engineState.resources.fonts[id.data].free();
engineState.resources.fonts.remove(id.data);
}
}
void unloadResource(AudioId id) {
if (engineState.resources.audios.has(id.data)) {
engineState.resources.audios[id.data].free();
engineState.resources.audios.remove(id.data);
}
}
/// Returns the opposite flip value.
/// The opposite of every flip value except none is none.
/// The fallback value is returned if the flip value is none.
@ -719,12 +808,6 @@ IStr toAssetsPath(IStr path) {
return pathConcat(assetsPath, path).pathFormat();
}
/// Loads a text file from the assets folder and returns its contents as a list.
/// Can handle both forward slashes and backslashes in file paths.
Result!LStr loadText(IStr path) {
return readText(path.toAssetsPath());
}
/// Loads a text file from the assets folder and returns its contents as a slice.
/// The slice can be safely used until this function is called again.
/// Can handle both forward slashes and backslashes in file paths.
@ -733,6 +816,33 @@ Result!IStr loadTempText(IStr path) {
return Result!IStr(engineState.tempText.items, fault);
}
Result!LStr loadRawText(IStr path) {
return readText(path.toAssetsPath());
}
/// Loads a text file from the assets folder and returns its contents as a list.
/// Can handle both forward slashes and backslashes in file paths.
Result!TextId loadText(IStr path) {
if (engineState.resources.textNames.length == 0) {
engineState.resources.textNames.append(LStr());
engineState.resources.texts.append(LStr());
}
foreach (id; engineState.resources.textNames.ids) {
if (engineState.resources.textNames[id] == path) {
return Result!TextId(TextId(id));
}
}
auto result = loadRawText(path);
if (result.isSome) {
engineState.resources.textNames.append(LStr(path));
return Result!TextId(TextId(engineState.resources.texts.append(result.unwrap())));
} else {
return Result!TextId(Fault.cantFind);
}
}
/// Loads an image file (PNG) from the assets folder.
/// Can handle both forward slashes and backslashes in file paths.
@trusted
@ -742,8 +852,20 @@ Result!Texture loadRawTexture(IStr path) {
}
Result!TextureId loadTexture(IStr path) {
if (engineState.resources.textureNames.length == 0) {
engineState.resources.textureNames.append(LStr());
engineState.resources.textures.append(Texture());
}
foreach (id; engineState.resources.textureNames.ids) {
if (engineState.resources.textureNames[id] == path) {
return Result!TextureId(TextureId(id));
}
}
auto result = loadRawTexture(path);
if (result.isSome) {
engineState.resources.textureNames.append(LStr(path));
return Result!TextureId(TextureId(engineState.resources.textures.append(result.unwrap())));
} else {
return Result!TextureId(Fault.cantFind);
@ -762,6 +884,17 @@ Result!Font loadRawFont(IStr path, uint size, const(dchar)[] runes = []) {
}
Result!FontId loadFont(IStr path, uint size, const(dchar)[] runes = []) {
if (engineState.resources.fontNames.length == 0) {
engineState.resources.fontNames.append(LStr());
engineState.resources.fonts.append(Font());
}
foreach (id; engineState.resources.fontNames.ids) {
if (engineState.resources.fontNames[id] == path) {
return Result!FontId(FontId(id));
}
}
auto result = loadRawFont(path, size, runes);
if (result.isSome) {
return Result!FontId(FontId(engineState.resources.fonts.append(result.unwrap())));
@ -770,30 +903,41 @@ Result!FontId loadFont(IStr path, uint size, const(dchar)[] runes = []) {
}
}
/// Loads a audio file (WAV, OGG, MP3) from the assets folder.
/// Loads a sound file (WAV, OGG, MP3) from the assets folder.
/// Can handle both forward slashes and backslashes in file paths.
@trusted
Result!Audio loadRawAudio(IStr path) {
auto value = Audio();
Result!Sound loadRawSound(IStr path) {
auto value = Sound();
if (path.endsWith(".wav")) {
value.data = rl.LoadSound(path.toAssetsPath().toCStr().unwrapOr());
} else {
value.data = rl.LoadMusicStream(path.toAssetsPath().toCStr().unwrapOr());
}
return Result!Audio(value, value.isEmpty.toFault(Fault.cantFind));
return Result!Sound(value, value.isEmpty.toFault(Fault.cantFind));
}
Result!AudioId loadAudio(IStr path) {
auto result = loadRawAudio(path);
Result!SoundId loadSound(IStr path) {
if (engineState.resources.soundNames.length == 0) {
engineState.resources.soundNames.append(LStr());
engineState.resources.sounds.append(Sound());
}
foreach (id; engineState.resources.soundNames.ids) {
if (engineState.resources.soundNames[id] == path) {
return Result!SoundId(SoundId(id));
}
}
auto result = loadRawSound(path);
if (result.isSome) {
return Result!AudioId(AudioId(engineState.resources.audios.append(result.unwrap())));
return Result!SoundId(SoundId(engineState.resources.sounds.append(result.unwrap())));
} else {
return Result!AudioId(Fault.cantFind);
return Result!SoundId(Fault.cantFind);
}
}
@trusted
Result!Viewport loadViewport(int width, int height) {
Result!Viewport loadRawViewport(int width, int height) {
auto value = rl.LoadRenderTexture(width, height).toPopka();
return Result!Viewport(value, value.isEmpty.toFault());
}
@ -804,6 +948,10 @@ Fault saveText(IStr path, IStr text) {
return writeText(path.toAssetsPath(), text);
}
void freeResources() {
engineState.resources.free();
}
/// Opens a window with the given size and title.
/// You should avoid calling this function manually.
@trusted
@ -811,13 +959,12 @@ void openWindow(int width, int height, IStr title = "Popka") {
if (rl.IsWindowReady) {
return;
}
rl.SetConfigFlags(rl.FLAG_VSYNC_HINT | rl.FLAG_WINDOW_RESIZABLE);
rl.SetConfigFlags(rl.FLAG_WINDOW_RESIZABLE | rl.FLAG_VSYNC_HINT);
rl.SetTraceLogLevel(rl.LOG_ERROR);
rl.InitWindow(width, height, title.toCStr().unwrapOr());
rl.InitAudioDevice();
rl.SetExitKey(rl.KEY_NULL);
lockFps(60);
engineState.flags.isVsync = true;
rl.SetTargetFPS(60);
engineState.backgroundColor = gray2;
engineState.fullscreenState.lastWindowSize = Vec2(width, height);
}
@ -826,11 +973,11 @@ void openWindow(int width, int height, IStr title = "Popka") {
/// This function will return when the loop function returns true.
/// You should avoid calling this function manually.
@trusted
void updateWindow(bool function() updateFunc) {
static bool function() @trusted @nogc nothrow __updateFunc;
void updateWindow(bool function(float dt) updateFunc) {
static bool function(float _dt) @trusted @nogc nothrow _updateFunc;
@trusted @nogc nothrow
static bool __updateWindow() {
static bool _updateWindow() {
// Begin drawing.
if (isResolutionLocked) {
rl.BeginTextureMode(engineState.viewport.toRl());
@ -840,7 +987,8 @@ void updateWindow(bool function() updateFunc) {
rl.ClearBackground(engineState.backgroundColor.toRl());
// The main loop.
auto result = __updateFunc();
auto result = _updateFunc(deltaTime);
engineState.tickCount = (engineState.tickCount + 1) % typeof(engineState.tickCount).max;
// End drawing.
if (isResolutionLocked) {
@ -874,7 +1022,7 @@ void updateWindow(bool function() updateFunc) {
if (engineState.viewport.isLockResolutionQueued) {
engineState.viewport.isLockResolutionQueued = false;
engineState.viewport.free();
engineState.viewport.data = loadViewport(engineState.viewport.targetWidth, engineState.viewport.targetHeight).unwrapOr();
engineState.viewport.data = loadRawViewport(engineState.viewport.targetWidth, engineState.viewport.targetHeight).unwrapOr();
} else if (engineState.viewport.isUnlockResolutionQueued) {
engineState.viewport.isUnlockResolutionQueued = false;
engineState.viewport.free();
@ -900,20 +1048,20 @@ void updateWindow(bool function() updateFunc) {
}
// Maybe bad idea, but makes life of no-attribute people easier.
__updateFunc = cast(bool function() @trusted @nogc nothrow) updateFunc;
_updateFunc = cast(bool function(float _dt) @trusted @nogc nothrow) updateFunc;
engineState.flags.isUpdating = true;
version(WebAssembly) {
static void __updateWindowWeb() {
if (__updateWindow()) {
static void _updateWindowWeb() {
if (_updateWindow()) {
engineState.flags.isUpdating = false;
rl.emscripten_cancel_main_loop();
}
}
rl.emscripten_set_main_loop(&__updateWindowWeb, 0, 1);
rl.emscripten_set_main_loop(&_updateWindowWeb, 0, 1);
} else {
while (true) {
if (rl.WindowShouldClose() || __updateWindow()) {
if (rl.WindowShouldClose() || _updateWindow()) {
engineState.flags.isUpdating = false;
break;
}
@ -949,25 +1097,6 @@ float masterVolume() {
return rl.GetMasterVolume();
}
/// Returns true if the FPS is locked.
bool isFpsLocked() {
return engineState.flags.isFpsLocked;
}
/// Locks the FPS to the given value.
@trusted
void lockFps(int target) {
engineState.flags.isFpsLocked = true;
rl.SetTargetFPS(target);
}
/// Unlocks the FPS.
@trusted
void unlockFps() {
engineState.flags.isFpsLocked = false;
rl.SetTargetFPS(0);
}
/// Returns true if the resolution is locked.
bool isResolutionLocked() {
return !engineState.viewport.isEmpty;
@ -977,7 +1106,7 @@ bool isResolutionLocked() {
@trusted
void lockResolution(int width, int height) {
if (!engineState.flags.isUpdating) {
engineState.viewport.data = loadViewport(width, height).unwrap();
engineState.viewport.data = loadRawViewport(width, height).unwrap();
} else {
engineState.viewport.targetWidth = width;
engineState.viewport.targetHeight = height;
@ -1150,6 +1279,10 @@ double elapsedTime() {
return rl.GetTime();
}
long elapsedTickCount() {
return engineState.tickCount;
}
@trusted
float deltaTime() {
return rl.GetFrameTime();
@ -1160,30 +1293,6 @@ Vec2 deltaMouse() {
return toPopka(rl.GetMouseDelta());
}
@trusted
void attachCamera(ref Camera camera) {
if (camera.isAttached) {
return;
}
camera.isAttached = true;
auto temp = camera.toRl();
if (isPixelPerfect) {
temp.target.x = floor(temp.target.x);
temp.target.y = floor(temp.target.y);
temp.offset.x = floor(temp.offset.x);
temp.offset.y = floor(temp.offset.y);
}
rl.BeginMode2D(temp);
}
@trusted
void detachCamera(ref Camera camera) {
if (camera.isAttached) {
camera.isAttached = false;
rl.EndMode2D();
}
}
@trusted
Vec2 measureTextSize(Font font, IStr text, DrawOptions options = DrawOptions()) {
if (font.isEmpty || text.length == 0) {
@ -1310,65 +1419,65 @@ Vec2 wasd() {
}
@trusted
void playAudio(Audio audio) {
if (audio.isEmpty) {
void playSound(Sound sound) {
if (sound.isEmpty) {
return;
}
if (audio.isSound) {
rl.PlaySound(audio.sound);
if (sound.isSound) {
rl.PlaySound(sound.data.get!(rl.Sound)());
} else {
rl.PlayMusicStream(audio.music);
rl.PlayMusicStream(sound.data.get!(rl.Music)());
}
}
@trusted
void updateAudio(Audio audio) {
if (audio.isEmpty) {
void updateSound(Sound sound) {
if (sound.isEmpty) {
return;
}
if (audio.isMusic) {
rl.UpdateMusicStream(audio.music);
if (sound.isMusic) {
rl.UpdateMusicStream(sound.data.get!(rl.Music)());
}
}
@trusted
void pauseAudio(Audio audio) {
if (audio.isEmpty) {
void pauseSound(Sound sound) {
if (sound.isEmpty) {
return;
}
if (audio.isSound) {
rl.PauseSound(audio.sound);
if (sound.isSound) {
rl.PauseSound(sound.data.get!(rl.Sound)());
} else {
rl.PauseMusicStream(audio.music);
rl.PauseMusicStream(sound.data.get!(rl.Music)());
}
}
@trusted
void resumeAudio(Audio audio) {
if (audio.isEmpty) {
void resumeSound(Sound sound) {
if (sound.isEmpty) {
return;
}
if (audio.isSound) {
rl.ResumeSound(audio.sound);
if (sound.isSound) {
rl.ResumeSound(sound.data.get!(rl.Sound)());
} else {
rl.ResumeMusicStream(audio.music);
rl.ResumeMusicStream(sound.data.get!(rl.Music)());
}
}
@trusted
void stopAudio(Audio audio) {
if (audio.isEmpty) {
void stopSound(Sound sound) {
if (sound.isEmpty) {
return;
}
if (audio.isSound) {
rl.StopSound(audio.sound);
if (sound.isSound) {
rl.StopSound(sound.data.get!(rl.Sound)());
} else {
rl.StopMusicStream(audio.music);
rl.StopMusicStream(sound.data.get!(rl.Music)());
}
}

View file

@ -431,11 +431,21 @@ struct Rectangle
this.height = height;
}
pragma(inline, true)
this(float width, float height) {
this(0.0f, 0.0f, width, height);
}
pragma(inline, true)
this(Vector2 position, Vector2 size) {
this(position.x, position.y, size.x, size.y);
}
pragma(inline, true)
this(Vector2 size) {
this(0.0f, 0.0f, size.x, size.y);
}
pragma(inline, true)
this(float x, float y, Vector2 size) {
this(x, y, size.x, size.y);

View file

@ -33,9 +33,17 @@ struct TileMap {
return Vec2(tileWidth, tileHeight);
}
int width() {
return cast(int) (colCount * tileWidth);
}
int height() {
return cast(int) (rowCount * tileHeight);
}
/// Returns the size of the tile map.
Vec2 size() {
return tileSize * Vec2(colCount, rowCount);
return Vec2(width, height);
}
Fault parse(IStr csv, int tileWidth, int tileHeight) {
@ -90,7 +98,7 @@ Result!TileMap toTileMap(IStr csv, int tileWidth, int tileHeight) {
return Result!TileMap(value, fault);
}
Result!TileMap loadTileMap(IStr path, int tileWidth, int tileHeight) {
Result!TileMap loadRawTileMap(IStr path, int tileWidth, int tileHeight) {
auto temp = loadTempText(path);
if (temp.isNone) {
return Result!TileMap(temp.fault);
@ -99,8 +107,8 @@ Result!TileMap loadTileMap(IStr path, int tileWidth, int tileHeight) {
}
void drawTile(Texture texture, Vec2 position, int tileID, int tileWidth, int tileHeight, DrawOptions options = DrawOptions()) {
auto gridWidth = cast(int) (texture.size.x / tileWidth);
auto gridHeight = cast(int) (texture.size.y / tileHeight);
auto gridWidth = cast(int) (texture.width / tileWidth);
auto gridHeight = cast(int) (texture.height / tileHeight);
if (gridWidth == 0 || gridHeight == 0) {
return;
}