parin/source/parin/engine.d
2025-04-11 19:34:24 +03:00

2466 lines
86 KiB
D
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// ---
// Copyright 2024 Alexandros F. G. Kapretsos
// SPDX-License-Identifier: MIT
// Email: alexandroskapretsos@gmail.com
// Project: https://github.com/Kapendev/parin
// Version: v0.0.41
// ---
/// The `engine` module functions as a lightweight 2D game engine.
module parin.engine;
import rl = parin.rl;
import joka.ascii;
import joka.io;
import joka.memory;
public import joka.containers;
public import joka.math;
public import joka.types;
@safe @nogc nothrow:
EngineState* engineState;
enum defaultEngineTexturesCapacity = 128;
enum defaultEngineSoundsCapacity = 128;
enum defaultEngineFontsCapacity = 16;
alias EngineFlags = ushort;
enum EngineFlag : EngineFlags {
none = 0x0000,
isUpdating = 0x0001,
isUsingAssetsPath = 0x0002,
isPixelSnapped = 0x0004,
isPixelPerfect = 0x0008,
isFullscreen = 0x0010,
isCursorVisible = 0x0020,
}
/// Flipping orientations.
enum Flip : ubyte {
none, /// No flipping.
x, /// Flipped along the X-axis.
y, /// Flipped along the Y-axis.
xy, /// Flipped along both X and Y axes.
}
/// Alignment orientations.
enum Alignment : ubyte {
left, /// Align to the left.
center, /// Align to the center.
right, /// Align to the right.
}
/// Texture filtering modes.
enum Filter : ubyte {
nearest = rl.TEXTURE_FILTER_POINT, /// Nearest neighbor filtering (blocky).
linear = rl.TEXTURE_FILTER_BILINEAR, /// Bilinear filtering (smooth).
}
/// Texture wrapping modes.
enum Wrap : ubyte {
clamp = rl.TEXTURE_WRAP_CLAMP, /// Clamps texture.
repeat = rl.TEXTURE_WRAP_REPEAT, /// Repeats texture.
}
/// Texture blending modes.
enum Blend : ubyte {
alpha = rl.BLEND_CUSTOM_SEPARATE, /// Standard alpha blending.
additive = rl.BLEND_ADDITIVE, /// Adds colors for light effects.
multiplied = rl.BLEND_MULTIPLIED, /// Multiplies colors for shadows.
add = rl.BLEND_ADD_COLORS, /// Simply adds colors.
sub = rl.BLEND_SUBTRACT_COLORS, /// Simply subtracts colors.
}
/// A limited set of keyboard keys.
enum Keyboard : ushort {
none = rl.KEY_NULL, /// Not a key.
a = rl.KEY_A, /// The A key.
b = rl.KEY_B, /// The B key.
c = rl.KEY_C, /// The C key.
d = rl.KEY_D, /// The D key.
e = rl.KEY_E, /// The E key.
f = rl.KEY_F, /// The F key.
g = rl.KEY_G, /// The G key.
h = rl.KEY_H, /// The H key.
i = rl.KEY_I, /// The I key.
j = rl.KEY_J, /// The J key.
k = rl.KEY_K, /// The K key.
l = rl.KEY_L, /// The L key.
m = rl.KEY_M, /// The M key.
n = rl.KEY_N, /// The N key.
o = rl.KEY_O, /// The O key.
p = rl.KEY_P, /// The P key.
q = rl.KEY_Q, /// The Q key.
r = rl.KEY_R, /// The R key.
s = rl.KEY_S, /// The S key.
t = rl.KEY_T, /// The T key.
u = rl.KEY_U, /// The U key.
v = rl.KEY_V, /// The V key.
w = rl.KEY_W, /// The W key.
x = rl.KEY_X, /// The X key.
y = rl.KEY_Y, /// The Y key.
z = rl.KEY_Z, /// The Z key.
n0 = rl.KEY_ZERO, /// The 0 key.
n1 = rl.KEY_ONE, /// The 1 key.
n2 = rl.KEY_TWO, /// The 2 key.
n3 = rl.KEY_THREE, /// The 3 key.
n4 = rl.KEY_FOUR, /// The 4 key.
n5 = rl.KEY_FIVE, /// The 5 key.
n6 = rl.KEY_SIX, /// The 6 key.
n7 = rl.KEY_SEVEN, /// The 7 key.
n8 = rl.KEY_EIGHT, /// The 8 key.
n9 = rl.KEY_NINE, /// The 9 key.
nn0 = rl.KEY_KP_0, /// The 0 key on the numpad.
nn1 = rl.KEY_KP_1, /// The 1 key on the numpad.
nn2 = rl.KEY_KP_2, /// The 2 key on the numpad.
nn3 = rl.KEY_KP_3, /// The 3 key on the numpad.
nn4 = rl.KEY_KP_4, /// The 4 key on the numpad.
nn5 = rl.KEY_KP_5, /// The 5 key on the numpad.
nn6 = rl.KEY_KP_6, /// The 6 key on the numpad.
nn7 = rl.KEY_KP_7, /// The 7 key on the numpad.
nn8 = rl.KEY_KP_8, /// The 8 key on the numpad.
nn9 = rl.KEY_KP_9, /// The 9 key on the numpad.
f1 = rl.KEY_F1, /// The f1 key.
f2 = rl.KEY_F2, /// The f2 key.
f3 = rl.KEY_F3, /// The f3 key.
f4 = rl.KEY_F4, /// The f4 key.
f5 = rl.KEY_F5, /// The f5 key.
f6 = rl.KEY_F6, /// The f6 key.
f7 = rl.KEY_F7, /// The f7 key.
f8 = rl.KEY_F8, /// The f8 key.
f9 = rl.KEY_F9, /// The f9 key.
f10 = rl.KEY_F10, /// The f10 key.
f11 = rl.KEY_F11, /// The f11 key.
f12 = rl.KEY_F12, /// The f12 key.
left = rl.KEY_LEFT, /// The left arrow key.
right = rl.KEY_RIGHT, /// The right arrow key.
up = rl.KEY_UP, /// The up arrow key.
down = rl.KEY_DOWN, /// The down arrow key.
esc = rl.KEY_ESCAPE, /// The escape key.
enter = rl.KEY_ENTER, /// The enter key.
tab = rl.KEY_TAB, /// The tab key.
space = rl.KEY_SPACE, /// The space key.
backspace = rl.KEY_BACKSPACE, /// THe backspace key.
shift = rl.KEY_LEFT_SHIFT, /// The left shift key.
ctrl = rl.KEY_LEFT_CONTROL, /// The left control key.
alt = rl.KEY_LEFT_ALT, /// The left alt key.
win = rl.KEY_LEFT_SUPER, /// The left windows/super/command key.
insert = rl.KEY_INSERT, /// The insert key.
del = rl.KEY_DELETE, /// The delete key.
home = rl.KEY_HOME, /// The home key.
end = rl.KEY_END, /// The end key.
pageUp = rl.KEY_PAGE_UP, /// The page up key.
pageDown = rl.KEY_PAGE_DOWN, /// The page down key.
}
/// A limited set of mouse keys.
enum Mouse : ushort {
none = 0, /// Not a button.
left = rl.MOUSE_BUTTON_LEFT + 1, /// The left mouse button.
right = rl.MOUSE_BUTTON_RIGHT + 1, /// The right mouse button.
middle = rl.MOUSE_BUTTON_MIDDLE + 1, /// The middle mouse button.
}
/// A limited set of gamepad buttons.
enum Gamepad : ushort {
none = rl.GAMEPAD_BUTTON_UNKNOWN, /// Not a button.
left = rl.GAMEPAD_BUTTON_LEFT_FACE_LEFT, /// The left button.
right = rl.GAMEPAD_BUTTON_LEFT_FACE_RIGHT, /// The right button.
up = rl.GAMEPAD_BUTTON_LEFT_FACE_UP, /// The up button.
down = rl.GAMEPAD_BUTTON_LEFT_FACE_DOWN, /// The down button.
y = rl.GAMEPAD_BUTTON_RIGHT_FACE_UP, /// The Xbox y, PlayStation triangle and Nintendo x button.
x = rl.GAMEPAD_BUTTON_RIGHT_FACE_RIGHT, /// The Xbox x, PlayStation square and Nintendo y button.
a = rl.GAMEPAD_BUTTON_RIGHT_FACE_DOWN, /// The Xbox a, PlayStation cross and Nintendo b button.
b = rl.GAMEPAD_BUTTON_RIGHT_FACE_LEFT, /// The Xbox b, PlayStation circle and Nintendo a button.
lt = rl.GAMEPAD_BUTTON_LEFT_TRIGGER_2, /// The left trigger button.
lb = rl.GAMEPAD_BUTTON_LEFT_TRIGGER_1, /// The left bumper button.
lsb = rl.GAMEPAD_BUTTON_LEFT_THUMB, /// The left stick button.
rt = rl.GAMEPAD_BUTTON_RIGHT_TRIGGER_2, /// The right trigger button.
rb = rl.GAMEPAD_BUTTON_RIGHT_TRIGGER_1, /// The right bumper button.
rsb = rl.GAMEPAD_BUTTON_RIGHT_THUMB, /// The right stick button.
back = rl.GAMEPAD_BUTTON_MIDDLE_LEFT, /// The back button.
start = rl.GAMEPAD_BUTTON_MIDDLE_RIGHT, /// The start button.
middle = rl.GAMEPAD_BUTTON_MIDDLE, /// The middle button.
}
/// Options for configuring drawing parameters.
struct DrawOptions {
Vec2 origin = Vec2(0.0f); /// The origin point of the drawn object. This value can be used to force a specific origin.
Vec2 scale = Vec2(1.0f); /// The scale of the drawn object.
float rotation = 0.0f; /// The rotation of the drawn object, in degrees.
Color color = white; /// The color of the drawn object, in RGBA.
Hook hook = Hook.topLeft; /// A value representing the origin point of the drawn object when origin is zero.
Flip flip = Flip.none; /// A value representing flipping orientations.
@safe @nogc nothrow:
/// Initializes the options with the given rotation.
this(float rotation) {
this.rotation = rotation;
}
/// Initializes the options with the given scale.
this(Vec2 scale) {
this.scale = scale;
}
/// Initializes the options with the given color.
this(Color color) {
this.color = color;
}
/// Initializes the options with the given hook.
this(Hook hook) {
this.hook = hook;
}
/// Initializes the options with the given flip.
this(Flip flip) {
this.flip = flip;
}
}
/// Options for configuring extra drawing parameters for text.
struct TextOptions {
float visibilityRatio = 1.0f; /// Controls the visibility ratio of the text when visibilityCount is zero, where 0.0 means fully hidden and 1.0 means fully visible.
int alignmentWidth = 0; /// The width of the aligned text. It is used as a hint and is not enforced.
ushort visibilityCount = 0; /// Controls the visibility count of the text. This value can be used to force a specific character count.
Alignment alignment = Alignment.left; /// A value represeting alignment orientations.
bool isRightToLeft = false; /// Indicates whether the content of the text flows in a right-to-left direction.
@safe @nogc nothrow:
/// Initializes the options with the given visibility ratio.
this(float visibilityRatio) {
this.visibilityRatio = visibilityRatio;
}
/// Initializes the options with the given alignment.
this(Alignment alignment, int alignmentWidth = 0) {
this.alignment = alignment;
this.alignmentWidth = alignmentWidth;
}
}
/// A texture resource.
struct Texture {
rl.Texture2D data;
@safe @nogc nothrow:
/// Checks if the texture is not loaded.
bool isEmpty() {
return data.id <= 0;
}
/// Returns the width of the texture.
int width() {
return data.width;
}
/// Returns the height of the texture.
int height() {
return data.height;
}
/// Returns the size of the texture.
Vec2 size() {
return Vec2(width, height);
}
/// Sets the filter mode of the texture.
@trusted
void setFilter(Filter value) {
if (isEmpty) return;
rl.SetTextureFilter(data, value);
}
/// Sets the wrap mode of the texture.
@trusted
void setWrap(Wrap value) {
if (isEmpty) return;
rl.SetTextureWrap(data, value);
}
/// Frees the loaded texture.
@trusted
void free() {
if (isEmpty) return;
rl.UnloadTexture(data);
this = Texture();
}
}
/// An identifier for a managed engine resource. Managed resources can be safely shared throughout the code.
/// To free these resources, use the `freeEngineResources` function or the `free` method on the identifier.
/// The identifier is automatically invalidated when the resource is freed.
struct TextureId {
GenerationalIndex data;
@safe @nogc nothrow:
/// Returns the width of the texture associated with the resource identifier.
int width() {
return getOr().width;
}
/// Returns the height of the texture associated with the resource identifier.
int height() {
return getOr().height;
}
/// Returns the size of the texture associated with the resource identifier.
Vec2 size() {
return getOr().size;
}
/// Sets the filter mode of the texture associated with the resource identifier.
void setFilter(Filter value) {
getOr().setFilter(value);
}
/// Sets the wrap mode of the texture associated with the resource identifier.
void setWrap(Wrap value) {
getOr().setWrap(value);
}
/// Checks if the resource identifier is valid. It becomes automatically invalid when the resource is freed.
bool isValid() {
return data.value && engineState.textures.has(GenerationalIndex(data.value - 1, data.generation));
}
/// Retrieves the texture associated with the resource identifier.
ref Texture get() {
if (!isValid) {
if (data.value) assert(0, "ID `{}` with generation `{}` does not exist.".format(data.value, data.generation));
else assert(0, "ID `0` is always invalid and represents a resource that was never created.");
}
return engineState.textures[GenerationalIndex(data.value - 1, data.generation)];
}
/// Retrieves the texture associated with the resource identifier or returns a default value if invalid.
Texture getOr() {
return isValid ? engineState.textures[GenerationalIndex(data.value - 1, data.generation)] : Texture();
}
/// Frees the resource associated with the identifier.
void free() {
if (isValid) engineState.textures.remove(GenerationalIndex(data.value - 1, data.generation));
}
}
/// A font resource.
struct Font {
rl.Font data;
int runeSpacing; /// The spacing between individual characters.
int lineSpacing; /// The spacing between lines of text.
@safe @nogc nothrow:
/// Checks if the font is not loaded.
bool isEmpty() {
return data.texture.id <= 0;
}
/// Returns the size of the font.
int size() {
return data.baseSize;
}
/// Sets the filter mode of the font.
@trusted
void setFilter(Filter value) {
if (isEmpty) return;
rl.SetTextureFilter(data.texture, value);
}
/// Sets the wrap mode of the font.
@trusted
void setWrap(Wrap value) {
if (isEmpty) return;
rl.SetTextureWrap(data.texture, value);
}
/// Frees the loaded font.
@trusted
void free() {
if (isEmpty) return;
rl.UnloadFont(data);
this = Font();
}
}
/// An identifier for a managed engine resource. Managed resources can be safely shared throughout the code.
/// To free these resources, use the `freeEngineResources` function or the `free` method on the identifier.
/// The identifier is automatically invalidated when the resource is freed.
struct FontId {
GenerationalIndex data;
@safe @nogc nothrow:
/// Returns the spacing between individual characters of the font associated with the resource identifier.
int runeSpacing() {
return getOr().runeSpacing;
}
/// Returns the spacing between lines of text of the font associated with the resource identifier.
int lineSpacing() {
return getOr().lineSpacing;
};
/// Returns the size of the font associated with the resource identifier.
int size() {
return getOr().size;
}
/// Sets the filter mode of the font associated with the resource identifier.
void setFilter(Filter value) {
getOr().setFilter(value);
}
/// Sets the wrap mode of the font associated with the resource identifier.
void setWrap(Wrap value) {
getOr().setWrap(value);
}
/// Checks if the resource identifier is valid. It becomes automatically invalid when the resource is freed.
bool isValid() {
return data.value && engineState.fonts.has(GenerationalIndex(data.value - 1, data.generation));
}
/// Retrieves the font associated with the resource identifier.
ref Font get() {
if (!isValid) {
if (data.value) assert(0, "ID `{}` with generation `{}` does not exist.".format(data.value, data.generation));
else assert(0, "ID `0` is always invalid and represents a resource that was never created.");
}
return engineState.fonts[GenerationalIndex(data.value - 1, data.generation)];
}
/// Retrieves the font associated with the resource identifier or returns a default value if invalid.
Font getOr() {
return isValid ? engineState.fonts[GenerationalIndex(data.value - 1, data.generation)] : Font();
}
/// Frees the resource associated with the identifier.
void free() {
if (isValid) engineState.fonts.remove(GenerationalIndex(data.value - 1, data.generation));
}
}
/// A sound resource.
struct Sound {
Union!(rl.Sound, rl.Music) data;
bool isPaused;
bool isLooping;
@safe @nogc nothrow:
/// Checks if the sound is not loaded.
bool isEmpty() {
if (data.isType!(rl.Sound)) {
return data.get!(rl.Sound)().stream.sampleRate == 0;
} else {
return data.get!(rl.Music)().stream.sampleRate == 0;
}
}
/// Returns true if the sound is playing.
@trusted
bool isPlaying() {
if (data.isType!(rl.Sound)) {
return rl.IsSoundPlaying(data.get!(rl.Sound)());
} else {
return rl.IsMusicStreamPlaying(data.get!(rl.Music)());
}
}
/// Returns the current playback time of the sound.
@trusted
float time() {
if (data.isType!(rl.Sound)) {
return 0.0f;
} else {
return rl.GetMusicTimePlayed(data.get!(rl.Music)());
}
}
/// Returns the total duration of the sound.
@trusted
float duration() {
if (data.isType!(rl.Sound)) {
return 0.0f;
} else {
return rl.GetMusicTimeLength(data.get!(rl.Music)());
}
}
/// Returns the progress of the sound.
float progress() {
if (duration == 0.0f) return 0.0f;
return time / duration;
}
/// Sets the volume level for the sound. One is the default value.
@trusted
void setVolume(float value) {
if (data.isType!(rl.Sound)) {
rl.SetSoundVolume(data.get!(rl.Sound)(), value);
} else {
rl.SetMusicVolume(data.get!(rl.Music)(), value);
}
}
/// Sets the pitch of the sound. One is the default value.
@trusted
void setPitch(float value) {
if (data.isType!(rl.Sound)) {
rl.SetSoundPitch(data.get!(rl.Sound)(), value);
} else {
rl.SetMusicPitch(data.get!(rl.Music)(), value);
}
}
/// Sets the stereo panning of the sound. One is the default value.
@trusted
void setPan(float value) {
if (data.isType!(rl.Sound)) {
rl.SetSoundPan(data.get!(rl.Sound)(), value);
} else {
rl.SetMusicPan(data.get!(rl.Music)(), value);
}
}
/// Frees the loaded sound.
@trusted
void free() {
if (isEmpty) return;
if (data.isType!(rl.Sound)) {
rl.UnloadSound(data.get!(rl.Sound)());
} else {
rl.UnloadMusicStream(data.get!(rl.Music)());
}
this = Sound();
}
}
/// An identifier for a managed engine resource. Managed resources can be safely shared throughout the code.
/// To free these resources, use the `freeEngineResources` function or the `free` method on the identifier.
/// The identifier is automatically invalidated when the resource is freed.
struct SoundId {
GenerationalIndex data;
@safe @nogc nothrow:
/// Returns true if the sound associated with the resource identifier is paused.
bool isPaused() {
return getOr().isPaused;
}
/// Returns true if the sound associated with the resource identifier is looping.
bool isLooping() {
return getOr().isLooping;
}
/// Returns true if the sound associated with the resource identifier is playing.
bool isPlaying() {
return getOr().isPlaying;
}
/// Returns the current playback time of the sound associated with the resource identifier.
float time() {
return getOr().time;
}
/// Returns the total duration of the sound associated with the resource identifier.
float duration() {
return getOr().duration;
}
/// Returns the progress of the sound associated with the resource identifier.
float progress() {
return getOr().progress;
}
/// Sets the volume level for the sound associated with the resource identifier. One is the default value.
void setVolume(float value) {
getOr().setVolume(value);
}
/// Sets the pitch for the sound associated with the resource identifier. One is the default value.
void setPitch(float value) {
getOr().setPitch(value);
}
/// Sets the stereo panning for the sound associated with the resource identifier. One is the default value.
void setPan(float value) {
getOr().setPan(value);
}
/// Sets the looping mode for the sound associated with the resource identifier.
void setIsLooping(bool value) {
if (isValid) get().isLooping = value;
}
/// Checks if the resource identifier is valid. It becomes automatically invalid when the resource is freed.
bool isValid() {
return data.value && engineState.sounds.has(GenerationalIndex(data.value - 1, data.generation));
}
/// Retrieves the sound associated with the resource identifier.
ref Sound get() {
if (!isValid) {
if (data.value) assert(0, "ID `{}` with generation `{}` does not exist.".format(data.value, data.generation));
else assert(0, "ID `0` is always invalid and represents a resource that was never created.");
}
return engineState.sounds[GenerationalIndex(data.value - 1, data.generation)];
}
/// Retrieves the sound associated with the resource identifier or returns a default value if invalid.
Sound getOr() {
return isValid ? engineState.sounds[GenerationalIndex(data.value - 1, data.generation)] : Sound();
}
/// Frees the resource associated with the identifier.
void free() {
if (isValid) engineState.sounds.remove(GenerationalIndex(data.value - 1, data.generation));
}
}
/// A viewing area for rendering.
struct Viewport {
rl.RenderTexture2D data;
Color color; /// The background color of the viewport.
Blend blend; /// A value representing blending modes.
bool isAttached; /// Indicates whether the viewport is currently in use.
@safe @nogc nothrow:
/// Initializes the viewport with the given size, background color and blend mode.
this(Color color, Blend blend = Blend.alpha) {
this.color = color;
this.blend = blend;
}
/// Checks if the viewport is not loaded.
bool isEmpty() {
return data.texture.id <= 0;
}
/// Returns the width of the viewport.
int width() {
return data.texture.width;
}
/// Returns the height of the viewport.
int height() {
return data.texture.height;
}
/// Returns the size of the viewport.
Vec2 size() {
return Vec2(width, height);
}
/// Resizes the viewport to the given width and height.
/// Internally, this allocates a new render texture, so avoid calling it while the viewport is in use.
@trusted
void resize(int newWidth, int newHeight) {
if (width == newWidth && height == newHeight) return;
if (!isEmpty) rl.UnloadRenderTexture(data);
if (newWidth <= 0 || newHeight <= 0) {
data = rl.RenderTexture2D();
return;
}
data = rl.LoadRenderTexture(newWidth, newHeight);
setFilter(engineState.defaultFilter);
setWrap(engineState.defaultWrap);
}
/// Attaches the viewport, making it active.
// NOTE: The engine viewport should not use this function.
@trusted
void attach() {
if (isEmpty) return;
if (engineState.userViewport.isAttached) {
assert(0, "Cannot attach viewport because another viewport is already attached.");
}
isAttached = true;
engineState.userViewport = this;
if (isResolutionLocked) rl.EndTextureMode();
rl.BeginTextureMode(data);
rl.ClearBackground(color.toRl());
rl.BeginBlendMode(blend);
}
/// Detaches the viewport, making it inactive.
// NOTE: The engine viewport should not use this function.
@trusted
void detach() {
if (isEmpty) return;
if (!isAttached) {
assert(0, "Cannot detach viewport because it is not the attached viewport.");
}
isAttached = false;
engineState.userViewport = Viewport();
rl.EndBlendMode();
rl.EndTextureMode();
if (isResolutionLocked) rl.BeginTextureMode(engineState.viewport.data.toRl());
}
/// Sets the filter mode of the viewport.
@trusted
void setFilter(Filter value) {
if (isEmpty) return;
rl.SetTextureFilter(data.texture, value);
}
/// Sets the wrap mode of the viewport.
@trusted
void setWrap(Wrap value) {
if (isEmpty) return;
rl.SetTextureWrap(data.texture, value);
}
/// Frees the loaded viewport.
@trusted
void free() {
if (isEmpty) return;
rl.UnloadRenderTexture(data);
this = Viewport();
}
}
/// A camera.
struct Camera {
Vec2 position; /// The position of the cammera.
float rotation = 0.0f; /// The rotation angle of the camera, in degrees.
float scale = 1.0f; /// The zoom level of the camera.
bool isCentered; /// Determines if the camera's origin is at the center instead of the top left.
bool isAttached; /// Indicates whether the camera is currently in use.
@safe @nogc nothrow:
/// Initializes the camera with the given position and optional centering.
this(float x, float y, bool isCentered = false) {
this.position.x = x;
this.position.y = y;
this.isCentered = isCentered;
}
/// Returns the current hook associated with the camera.
Hook hook() {
return isCentered ? Hook.center : Hook.topLeft;
}
/// Returns the origin of the camera.
Vec2 origin(Viewport viewport = Viewport()) {
if (viewport.isEmpty) {
return Rect(resolution / Vec2(scale)).origin(hook);
} else {
return Rect(viewport.size / Vec2(scale)).origin(hook);
}
}
/// Returns the area covered by the camera.
Rect area(Viewport viewport = Viewport()) {
if (viewport.isEmpty) {
return Rect(position, resolution / Vec2(scale)).area(hook);
} else {
return Rect(position, viewport.size / Vec2(scale)).area(hook);
}
}
/// Returns the top left point of the camera.
Vec2 topLeftPoint() {
return area.topLeftPoint;
}
/// Returns the top point of the camera.
Vec2 topPoint() {
return area.topPoint;
}
/// Returns the top right point of the camera.
Vec2 topRightPoint() {
return area.topRightPoint;
}
/// Returns the left point of the camera.
Vec2 leftPoint() {
return area.leftPoint;
}
/// Returns the center point of the camera.
Vec2 centerPoint() {
return area.centerPoint;
}
/// Returns the right point of the camera.
Vec2 rightPoint() {
return area.rightPoint;
}
/// Returns the bottom left point of the camera.
Vec2 bottomLeftPoint() {
return area.bottomLeftPoint;
}
/// Returns the bottom point of the camera.
Vec2 bottomPoint() {
return area.bottomPoint;
}
/// Returns the bottom right point of the camera.
Vec2 bottomRightPoint() {
return area.bottomRightPoint;
}
/// Moves the camera to follow the target position at the specified speed.
void followPosition(Vec2 target, float speed) {
position = position.moveTo(target, Vec2(speed));
}
/// Moves the camera to follow the target position with gradual slowdown.
void followPositionWithSlowdown(Vec2 target, float slowdown) {
position = position.moveToWithSlowdown(target, Vec2(deltaTime), slowdown);
}
/// Adjusts the cameras zoom level to follow the target value at the specified speed.
void followScale(float target, float speed) {
scale = scale.moveTo(target, speed);
}
/// Adjusts the cameras zoom level to follow the target value with gradual slowdown.
void followScaleWithSlowdown(float target, float slowdown) {
scale = scale.moveToWithSlowdown(target, deltaTime, slowdown);
}
/// Attaches the camera, making it active.
@trusted
void attach() {
if (engineState.userCamera.isAttached) {
assert(0, "Cannot attach camera because another camera is already attached.");
}
isAttached = true;
engineState.userCamera = this;
auto temp = this.toRl(engineState.userViewport);
if (isPixelSnapped) {
temp.target.x = temp.target.x.floor();
temp.target.y = temp.target.y.floor();
temp.offset.x = temp.offset.x.floor();
temp.offset.y = temp.offset.y.floor();
}
rl.BeginMode2D(temp);
}
/// Detaches the camera, making it inactive.
@trusted
void detach() {
if (!isAttached) {
assert(0, "Cannot detach camera because it is not the attached camera.");
}
isAttached = false;
engineState.userCamera = Camera();
rl.EndMode2D();
}
}
/// Information about the engine viewport, including its area.
struct EngineViewportInfo {
Rect area; /// The area covered by the viewport.
Vec2 minSize; /// The minimum size that the viewport can be.
Vec2 maxSize; /// The maximum size that the viewport can be.
float minRatio = 0.0f; /// The minimum ratio between minSize and maxSize.
}
/// The engine viewport.
struct EngineViewport {
Viewport data; /// The viewport data.
int lockWidth; /// The target lock width.
int lockHeight; /// The target lock height.
bool isChanging; /// The flag that triggers the lock state.
}
/// The engine fullscreen state.
struct EngineFullscreenState {
int previousWindowWidth; /// The previous window with before entering fullscreen mode.
int previousWindowHeight; /// The previous window height before entering fullscreen mode.
float changeTime = 0.0f; /// The current change time.
bool isChanging; /// The flag that triggers the fullscreen state.
enum changeDuration = 0.03f;
}
/// The engine state.
struct EngineState {
EngineFlags flags;
EngineFullscreenState fullscreenState;
EngineViewportInfo viewportInfoBuffer;
Vec2 mouseBuffer;
Sz tickCount;
Color borderColor;
Filter defaultFilter;
Wrap defaultWrap;
Camera userCamera;
Viewport userViewport;
EngineViewport viewport;
GenerationalList!Texture textures;
GenerationalList!Sound sounds;
GenerationalList!Font fonts;
Font debugFont;
List!IStr envArgsBuffer;
List!IStr droppedFilePathsBuffer;
LStr loadTextBuffer;
LStr saveTextBuffer;
LStr assetsPath;
}
/// Converts a raylib type to a Parin type.
pragma(inline, true);
Color toParin(rl.Color from) {
return Color(from.r, from.g, from.b, from.a);
}
/// Converts a raylib type to a Parin type.
pragma(inline, true);
Vec2 toParin(rl.Vector2 from) {
return Vec2(from.x, from.y);
}
/// Converts a raylib type to a Parin type.
pragma(inline, true);
Vec3 toParin(rl.Vector3 from) {
return Vec3(from.x, from.y, from.z);
}
/// Converts a raylib type to a Parin type.
pragma(inline, true);
Vec4 toParin(rl.Vector4 from) {
return Vec4(from.x, from.y, from.z, from.w);
}
/// Converts a raylib type to a Parin type.
pragma(inline, true);
Rect toParin(rl.Rectangle from) {
return Rect(from.x, from.y, from.width, from.height);
}
/// Converts a raylib type to a Parin type.
pragma(inline, true);
Texture toParin(rl.Texture2D from) {
return Texture(from);
}
/// Converts a raylib type to a Parin type.
pragma(inline, true);
Font toParin(rl.Font from) {
return Font(from);
}
/// Converts a Parin type to a raylib type.
pragma(inline, true);
rl.Color toRl(Color from) {
return rl.Color(from.r, from.g, from.b, from.a);
}
/// Converts a Parin type to a raylib type.
pragma(inline, true);
rl.Vector2 toRl(Vec2 from) {
return rl.Vector2(from.x, from.y);
}
/// Converts a Parin type to a raylib type.
pragma(inline, true);
rl.Vector3 toRl(Vec3 from) {
return rl.Vector3(from.x, from.y, from.z);
}
/// Converts a Parin type to a raylib type.
pragma(inline, true);
rl.Vector4 toRl(Vec4 from) {
return rl.Vector4(from.x, from.y, from.z, from.w);
}
/// Converts a Parin type to a raylib type.
pragma(inline, true);
rl.Rectangle toRl(Rect from) {
return rl.Rectangle(from.position.x, from.position.y, from.size.x, from.size.y);
}
/// Converts a Parin type to a raylib type.
pragma(inline, true);
rl.Texture2D toRl(Texture from) {
return from.data;
}
/// Converts a Parin type to a raylib type.
pragma(inline, true);
rl.Font toRl(Font from) {
return from.data;
}
/// Converts a Parin type to a raylib type.
pragma(inline, true);
rl.RenderTexture2D toRl(Viewport from) {
return from.data;
}
/// Converts a Parin type to a raylib type.
pragma(inline, true);
rl.Camera2D toRl(Camera camera, Viewport viewport = Viewport()) {
return rl.Camera2D(
Rect(viewport.isEmpty ? resolution : viewport.size).origin(camera.isCentered ? Hook.center : Hook.topLeft).toRl(),
camera.position.toRl(),
camera.rotation,
camera.scale,
);
}
/// Converts an ASCII bitmap font texture into a font.
/// The texture will be freed when the font is freed.
// NOTE: The number of items allocated is calculated as: (font width / tile width) * (font height / tile height)
// NOTE: It uses the raylib allocator.
@trusted
Font toFont(Texture texture, int tileWidth, int tileHeight) {
if (texture.isEmpty || tileWidth <= 0|| tileHeight <= 0) return Font();
auto result = Font();
result.lineSpacing = tileHeight;
auto rowCount = texture.height / tileHeight;
auto colCount = texture.width / tileWidth;
auto maxCount = rowCount * colCount;
result.data.baseSize = tileHeight;
result.data.glyphCount = maxCount;
result.data.glyphPadding = 0;
result.data.texture = texture.data;
result.data.recs = cast(rl.Rectangle*) rl.MemAlloc(cast(uint) (maxCount * rl.Rectangle.sizeof));
foreach (i; 0 .. maxCount) {
result.data.recs[i].x = (i % colCount) * tileWidth;
result.data.recs[i].y = (i / colCount) * tileHeight;
result.data.recs[i].width = tileWidth;
result.data.recs[i].height = tileHeight;
}
result.data.glyphs = cast(rl.GlyphInfo*) rl.MemAlloc(cast(uint) (maxCount * rl.GlyphInfo.sizeof));
foreach (i; 0 .. maxCount) {
result.data.glyphs[i] = rl.GlyphInfo();
result.data.glyphs[i].value = i + 32;
}
return result;
}
/// 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.
Flip oppositeFlip(Flip flip, Flip fallback) {
return flip == fallback ? Flip.none : fallback;
}
/// Returns the arguments that this application was started with.
IStr[] envArgs() {
return engineState.envArgsBuffer[];
}
/// Returns a random integer between 0 and int.max (inclusive).
@trusted
int randi() {
return rl.GetRandomValue(0, int.max);
}
/// Returns a random floating point number between 0.0 and 1.0 (inclusive).
@trusted
float randf() {
return rl.GetRandomValue(0, cast(int) float.max) / cast(float) cast(int) float.max;
}
/// Sets the seed of the random number generator to the given value.
@trusted
void randomize(int seed) {
rl.SetRandomSeed(seed);
}
/// Randomizes the seed of the random number generator.
void randomize() {
randomize(randi);
}
/// Converts a world point to a screen point based on the given camera.
@trusted
Vec2 toScreenPoint(Vec2 position, Camera camera, Viewport viewport = Viewport()) {
return toParin(rl.GetWorldToScreen2D(position.toRl(), camera.toRl(viewport)));
}
/// Converts a screen point to a world point based on the given camera.
@trusted
Vec2 toWorldPoint(Vec2 position, Camera camera, Viewport viewport = Viewport()) {
return toParin(rl.GetScreenToWorld2D(position.toRl(), camera.toRl(viewport)));
}
/// Returns the path of the assets folder.
IStr assetsPath() {
return engineState.assetsPath.items;
}
/// Sets the path of the assets folder.
void setAssetsPath(IStr path) {
engineState.assetsPath.clear();
engineState.assetsPath.append(path);
}
/// Converts a path to a path within the assets folder.
IStr toAssetsPath(IStr path) {
if (!isUsingAssetsPath) return path;
return pathConcat(assetsPath, path).pathFormat();
}
/// Returns the dropped file paths of the current frame.
@trusted
IStr[] droppedFilePaths() {
return engineState.droppedFilePathsBuffer[];
}
/// Returns a reference to a cleared temporary text container.
/// The resource remains valid until this function is called again.
ref LStr prepareTempText() {
engineState.saveTextBuffer.clear();
return engineState.saveTextBuffer;
}
/// Loads a text file from the assets folder and saves the content into the given buffer.
/// Supports both forward slashes and backslashes in file paths.
Fault loadRawTextIntoBuffer(IStr path, ref LStr buffer) {
auto targetPath = isUsingAssetsPath ? path.toAssetsPath() : path;
return readTextIntoBuffer(targetPath, buffer);
}
/// Loads a text file from the assets folder.
/// Supports both forward slashes and backslashes in file paths.
Result!LStr loadRawText(IStr path) {
auto targetPath = isUsingAssetsPath ? path.toAssetsPath() : path;
return readText(targetPath);
}
/// Loads a text file from the assets folder.
/// The resource remains valid until this function is called again.
/// Supports both forward slashes and backslashes in file paths.
Result!IStr loadTempText(IStr path) {
auto fault = loadRawTextIntoBuffer(path, engineState.loadTextBuffer);
return Result!IStr(engineState.loadTextBuffer.items, fault);
}
/// Loads a texture file (PNG) from the assets folder.
/// Supports both forward slashes and backslashes in file paths.
@trusted
Result!Texture loadRawTexture(IStr path) {
auto targetPath = isUsingAssetsPath ? path.toAssetsPath() : path;
auto value = rl.LoadTexture(targetPath.toCStr().getOr()).toParin();
value.setFilter(engineState.defaultFilter);
value.setWrap(engineState.defaultWrap);
return Result!Texture(value, value.isEmpty.toFault(Fault.cantFind));
}
/// Loads a texture file (PNG) from the assets folder.
/// The resource can be safely shared throughout the code and is automatically invalidated when the resource is freed.
/// Supports both forward slashes and backslashes in file paths.
TextureId loadTexture(IStr path) {
auto resource = loadRawTexture(path);
if (resource.isNone) return TextureId();
auto id = TextureId(engineState.textures.append(resource.get()));
id.data.value += 1;
return id;
}
/// Loads a font file (TTF, OTF) from the assets folder.
/// Supports both forward slashes and backslashes in file paths.
@trusted
Result!Font loadRawFont(IStr path, int size, int runeSpacing, int lineSpacing, IStr32 runes = "") {
auto targetPath = isUsingAssetsPath ? path.toAssetsPath() : path;
auto value = rl.LoadFontEx(targetPath.toCStr().getOr(), size, runes == "" ? null : cast(int*) runes.ptr, cast(int) runes.length).toParin();
if (value.data.texture.id == rl.GetFontDefault().texture.id) {
value = Font();
}
value.runeSpacing = runeSpacing;
value.lineSpacing = lineSpacing;
value.setFilter(engineState.defaultFilter);
value.setWrap(engineState.defaultWrap);
return Result!Font(value, value.isEmpty.toFault(Fault.cantFind));
}
/// Loads a font file (TTF, OTF) from the assets folder.
/// The resource can be safely shared throughout the code and is automatically invalidated when the resource is freed.
/// Supports both forward slashes and backslashes in file paths.
FontId loadFont(IStr path, int size, int runeSpacing, int lineSpacing, IStr32 runes = "") {
auto resource = loadRawFont(path, size, runeSpacing, lineSpacing, runes);
if (resource.isNone) return FontId();
auto id = FontId(engineState.fonts.append(resource.get()));
id.data.value += 1;
return id;
}
/// Loads an ASCII bitmap font file (PNG) from the assets folder.
/// Supports both forward slashes and backslashes in file paths.
// NOTE: The number of items allocated for this font is calculated as: (font width / tile width) * (font height / tile height)
Result!Font loadRawFontFromTexture(IStr path, int tileWidth, int tileHeight) {
auto value = loadRawTexture(path).getOr();
return Result!Font(value.toFont(tileWidth, tileHeight), value.isEmpty.toFault(Fault.cantFind));
}
/// Loads an ASCII bitmap font file (PNG) from the assets folder.
/// The resource can be safely shared throughout the code and is automatically invalidated when the resource is freed.
/// Supports both forward slashes and backslashes in file paths.
// NOTE: The number of items allocated for this font is calculated as: (font width / tile width) * (font height / tile height)
FontId loadFontFromTexture(IStr path, int tileWidth, int tileHeight) {
auto resource = loadRawFontFromTexture(path, tileWidth, tileHeight);
if (resource.isNone) return FontId();
auto id = FontId(engineState.fonts.append(resource.get()));
id.data.value += 1;
return id;
}
/// Loads a sound file (WAV, OGG, MP3) from the assets folder.
/// Supports both forward slashes and backslashes in file paths.
@trusted
Result!Sound loadRawSound(IStr path, float volume, float pitch, bool isLooping) {
auto targetPath = isUsingAssetsPath ? path.toAssetsPath() : path;
auto value = Sound();
if (path.endsWith(".wav")) {
value.data = rl.LoadSound(targetPath.toCStr().getOr());
} else {
value.data = rl.LoadMusicStream(targetPath.toCStr().getOr());
}
value.isLooping = isLooping;
value.setVolume(volume);
value.setPitch(pitch);
return Result!Sound(value, value.isEmpty.toFault(Fault.cantFind));
}
/// Loads a sound file (WAV, OGG, MP3) from the assets folder.
/// The resource can be safely shared throughout the code and is automatically invalidated when the resource is freed.
/// Supports both forward slashes and backslashes in file paths.
SoundId loadSound(IStr path, float volume, float pitch, bool isLooping) {
auto resource = loadRawSound(path, volume, pitch, isLooping);
if (resource.isNone) return SoundId();
auto id = SoundId(engineState.sounds.append(resource.get()));
id.data.value += 1;
return id;
}
/// Saves a text file to the assets folder.
/// Supports both forward slashes and backslashes in file paths.
Fault saveText(IStr path, IStr text) {
auto targetPath = isUsingAssetsPath ? path.toAssetsPath() : path;
return writeText(targetPath, text);
}
/// Frees all managed engine resources.
void freeEngineResources() {
engineState.textures.free();
engineState.fonts.free();
engineState.sounds.free();
}
/// Opens a URL in the default web browser (if available).
/// Redirect to Parin's GitHub when no URL is provided.
@trusted
void openUrl(IStr url = "https://github.com/Kapendev/parin") {
rl.OpenURL(url.toCStr().getOr());
}
/// Opens a window with the specified size and title.
/// You should avoid calling this function manually.
@trusted
void openWindow(int width, int height, const(IStr)[] args, IStr title = "Parin") {
if (rl.IsWindowReady) return;
// Raylib stuff.
rl.SetConfigFlags(rl.FLAG_WINDOW_RESIZABLE | rl.FLAG_VSYNC_HINT);
rl.SetTraceLogLevel(rl.LOG_ERROR);
rl.InitWindow(width, height, title.toCStr().getOr());
rl.InitAudioDevice();
rl.SetExitKey(rl.KEY_NULL);
rl.SetTargetFPS(60);
rl.SetWindowMinSize(240, 135);
rl.rlSetBlendFactorsSeparate(0x0302, 0x0303, 1, 0x0303, 0x8006, 0x8006);
// Engine stuff.
engineState = cast(EngineState*) jokaMalloc(EngineState.sizeof);
jokaMemset(engineState, 0, EngineState.sizeof);
engineState.flags |= EngineFlag.isUsingAssetsPath;
engineState.borderColor = black;
engineState.defaultFilter = Filter.init;
engineState.defaultWrap = Wrap.init;
engineState.fullscreenState.previousWindowWidth = width;
engineState.fullscreenState.previousWindowHeight = height;
engineState.viewport.data.color = gray;
engineState.viewport.data.blend = Blend.init;
engineViewportInfo(true);
// Ready resources.
if (args.length) {
foreach (arg; args) engineState.envArgsBuffer.append(arg);
engineState.assetsPath.append(pathConcat(args[0].pathDirName, "assets"));
}
engineState.loadTextBuffer.reserve(8192);
engineState.saveTextBuffer.reserve(8192);
engineState.droppedFilePathsBuffer.reserve(defaultEngineFontsCapacity);
engineState.textures.reserve(defaultEngineTexturesCapacity);
engineState.sounds.reserve(defaultEngineSoundsCapacity);
engineState.fonts.reserve(defaultEngineFontsCapacity);
// Load debug font.
auto monogramData = cast(const(ubyte)[]) import("parin/monogram.png");
auto monogramImage = rl.LoadImageFromMemory(".png", monogramData.ptr, cast(int) monogramData.length);
auto monogramTexture = rl.LoadTextureFromImage(monogramImage);
engineState.debugFont = monogramTexture.toParin().toFont(6, 12);
rl.UnloadImage(monogramImage);
}
/// Passes C strings to the window arguments.
/// You should avoid calling this function manually.
@trusted
void openWindowExtraStep(int argc, immutable(char)** argv) {
engineState.envArgsBuffer.clear();
foreach (i; 0 .. argc) engineState.envArgsBuffer.append(argv[i].cStrToStr());
if (engineState.envArgsBuffer.length) engineState.assetsPath.append(pathConcat(engineState.envArgsBuffer[0].pathDirName, "assets"));
}
/// Updates the window every frame with the given function.
/// This function will return when the given function returns true.
/// You should avoid calling this function manually.
@trusted
void updateWindow(bool function(float dt) updateFunc) {
static bool function(float _dt) @trusted @nogc nothrow __updateFunc;
@trusted @nogc nothrow
static bool __updateWindow() {
// Begin drawing.
if (isResolutionLocked) {
rl.BeginTextureMode(engineState.viewport.data.toRl());
} else {
rl.BeginDrawing();
}
rl.ClearBackground(engineState.viewport.data.color.toRl());
// The main loop.
if (rl.IsFileDropped) {
auto list = rl.LoadDroppedFiles();
foreach (i; 0 .. list.count) {
engineState.droppedFilePathsBuffer.append(list.paths[i].toStr());
}
}
if (isResolutionLocked) {
auto rlMouse = rl.GetTouchPosition(0);
auto info = engineViewportInfo;
engineState.mouseBuffer = Vec2(
(rlMouse.x - (info.maxSize.x - info.area.size.x) * 0.5f) / info.minRatio,
(rlMouse.y - (info.maxSize.y - info.area.size.y) * 0.5f) / info.minRatio,
);
} else {
engineState.mouseBuffer = rl.GetTouchPosition(0).toParin();
}
auto dt = deltaTime;
auto result = __updateFunc(dt);
engineState.tickCount = (engineState.tickCount + 1) % engineState.tickCount.max;
if (rl.IsFileDropped) {
// NOTE: LoadDroppedFiles just returns a global variable.
rl.UnloadDroppedFiles(rl.LoadDroppedFiles());
engineState.droppedFilePathsBuffer.clear();
}
// End drawing.
if (isResolutionLocked) {
auto info = engineViewportInfo;
rl.EndTextureMode();
rl.BeginDrawing();
rl.ClearBackground(engineState.borderColor.toRl());
rl.DrawTexturePro(
engineState.viewport.data.toRl().texture,
rl.Rectangle(0.0f, 0.0f, info.minSize.x, -info.minSize.y),
info.area.toRl(),
rl.Vector2(0.0f, 0.0f),
0.0f,
rl.Color(255, 255, 255, 255),
);
rl.EndDrawing();
} else {
rl.EndDrawing();
}
// Viewport code.
if (engineState.viewport.isChanging) {
if (isResolutionLocked) {
auto temp = engineState.viewport.data.color;
engineState.viewport.data.free();
engineState.viewport.data.color = temp;
} else {
engineState.viewport.data.resize(engineState.viewport.lockWidth, engineState.viewport.lockHeight);
}
engineState.viewport.isChanging = false;
engineViewportInfo(true);
}
// Fullscreen code.
if (engineState.fullscreenState.isChanging) {
engineState.fullscreenState.changeTime += dt;
if (engineState.fullscreenState.changeTime >= engineState.fullscreenState.changeDuration) {
if (rl.IsWindowFullscreen()) {
rl.ToggleFullscreen();
// Size is first because raylib likes that. I will make raylib happy.
rl.SetWindowSize(
engineState.fullscreenState.previousWindowWidth,
engineState.fullscreenState.previousWindowHeight,
);
rl.SetWindowPosition(
cast(int) (screenWidth * 0.5f - engineState.fullscreenState.previousWindowWidth * 0.5f),
cast(int) (screenHeight * 0.5f - engineState.fullscreenState.previousWindowHeight * 0.5f),
);
} else {
rl.ToggleFullscreen();
}
engineState.fullscreenState.isChanging = false;
}
}
return result;
}
// Maybe bad idea, but makes life of no-attribute people easier.
__updateFunc = cast(bool function(float _dt) @trusted @nogc nothrow) updateFunc;
engineState.flags |= EngineFlag.isUpdating;
version(WebAssembly) {
static void __updateWindowWeb() {
if (__updateWindow()) {
rl.emscripten_cancel_main_loop();
}
}
rl.emscripten_set_main_loop(&__updateWindowWeb, 0, 1);
} else {
while (true) {
if (rl.WindowShouldClose() || __updateWindow()) {
break;
}
}
}
engineState.flags &= ~EngineFlag.isUpdating;
}
/// Closes the window.
/// You should avoid calling this function manually.
@trusted
void closeWindow() {
if (!rl.IsWindowReady()) return;
freeEngineResources();
engineState.viewport.data.free();
engineState.debugFont.free();
engineState.envArgsBuffer.free();
engineState.droppedFilePathsBuffer.free();
engineState.loadTextBuffer.free();
engineState.saveTextBuffer.free();
engineState.assetsPath.free();
jokaFree(engineState);
engineState = null;
rl.CloseAudioDevice();
rl.CloseWindow();
}
/// Returns true if the assets path is currently in use when loading.
bool isUsingAssetsPath() {
return cast(bool) (engineState.flags & EngineFlag.isUsingAssetsPath);
}
/// Sets whether the assets path should be in use when loading.
void setIsUsingAssetsPath(bool value) {
engineState.flags = value
? engineState.flags | EngineFlag.isUsingAssetsPath
: engineState.flags & ~EngineFlag.isUsingAssetsPath;
}
/// Returns true if the drawing is snapped to pixel coordinates.
bool isPixelSnapped() {
return cast(bool) (engineState.flags & EngineFlag.isPixelSnapped);
}
/// Sets whether drawing should be snapped to pixel coordinates.
void setIsPixelSnapped(bool value) {
engineState.flags = value
? engineState.flags | EngineFlag.isPixelSnapped
: engineState.flags & ~EngineFlag.isPixelSnapped;
}
/// Returns true if the drawing is done in a pixel perfect way.
bool isPixelPerfect() {
return cast(bool) (engineState.flags & EngineFlag.isPixelPerfect);
}
/// Sets whether drawing should be done in a pixel-perfect way.
void setIsPixelPerfect(bool value) {
engineState.flags = value
? engineState.flags | EngineFlag.isPixelPerfect
: engineState.flags & ~EngineFlag.isPixelPerfect;
}
/// Returns true if the application is currently in fullscreen mode.
// NOTE: There is a conflict between the flag and real-window-state, which could potentially cause issues for some users.
@trusted
bool isFullscreen() {
return cast(bool) (engineState.flags & EngineFlag.isFullscreen);
}
/// Sets whether the application should be in fullscreen mode.
// NOTE: This function introduces a slight delay to prevent some bugs observed on Linux. See the `updateWindow` function.
@trusted
void setIsFullscreen(bool value) {
if (value == isFullscreen || engineState.fullscreenState.isChanging) return;
engineState.flags = value
? engineState.flags | EngineFlag.isFullscreen
: engineState.flags & ~EngineFlag.isFullscreen;
version(WebAssembly) {
} else {
if (value) {
engineState.fullscreenState.previousWindowWidth = rl.GetScreenWidth();
engineState.fullscreenState.previousWindowHeight = rl.GetScreenHeight();
rl.SetWindowPosition(0, 0);
rl.SetWindowSize(screenWidth, screenHeight);
}
engineState.fullscreenState.changeTime = 0.0f;
engineState.fullscreenState.isChanging = true;
}
}
/// Toggles the fullscreen mode on or off.
void toggleIsFullscreen() {
setIsFullscreen(!isFullscreen);
}
/// Returns true if the cursor is currently visible.
bool isCursorVisible() {
return cast(bool) (engineState.flags & EngineFlag.isCursorVisible);
}
/// Sets whether the cursor should be visible or hidden.
@trusted
void setIsCursorVisible(bool value) {
engineState.flags = value
? engineState.flags | EngineFlag.isCursorVisible
: engineState.flags & ~EngineFlag.isCursorVisible;
if (value) rl.ShowCursor();
else rl.HideCursor();
}
/// Toggles the visibility of the cursor.
void toggleIsCursorVisible() {
setIsCursorVisible(!isCursorVisible);
}
/// Returns true if the windows was resized.
@trusted
bool isWindowResized() {
return rl.IsWindowResized();
}
/// Sets the background color to the specified value.
void setBackgroundColor(Color value) {
engineState.viewport.data.color = value;
}
/// Sets the border color to the specified value.
void setBorderColor(Color value) {
engineState.borderColor = value;
}
/// Sets the minimum size of the window to the specified value.
@trusted
void setWindowMinSize(int width, int height) {
rl.SetWindowMinSize(width, height);
}
/// Sets the maximum size of the window to the specified value.
@trusted
void setWindowMaxSize(int width, int height) {
rl.SetWindowMaxSize(width, height);
}
/// Sets the window icon to the specified image that will be loaded from the assets folder.
/// Supports both forward slashes and backslashes in file paths.
@trusted
Fault setWindowIconFromFiles(IStr path) {
auto targetPath = isUsingAssetsPath ? path.toAssetsPath() : path;
auto image = rl.LoadImage(targetPath.toCStr().getOr());
if (image.data == null) return Fault.cantFind;
rl.SetWindowIcon(image);
rl.UnloadImage(image);
return Fault.none;
}
/// Returns information about the engine viewport, including its area.
EngineViewportInfo engineViewportInfo(bool isRecalculationForced = false) {
auto result = &engineState.viewportInfoBuffer;
if (!isRecalculationForced && !isWindowResized) return *result;
if (isResolutionLocked) {
result.minSize = resolution;
result.maxSize = windowSize;
auto ratio = result.maxSize / result.minSize;
result.minRatio = min(ratio.x, ratio.y);
if (isPixelPerfect) {
auto roundMinRatio = result.minRatio.round();
auto floorMinRation = result.minRatio.floor();
result.minRatio = result.minRatio.equals(roundMinRatio, 0.015f) ? roundMinRatio : floorMinRation;
}
auto targetSize = result.minSize * Vec2(result.minRatio);
auto targetPosition = result.maxSize * Vec2(0.5f) - targetSize * Vec2(0.5f);
result.area = Rect(
targetPosition.floor(),
ratio.x == result.minRatio ? targetSize.x : floor(targetSize.x),
ratio.y == result.minRatio ? targetSize.y : floor(targetSize.y),
);
} else {
result.minSize = windowSize;
result.maxSize = result.minSize;
result.minRatio = 1.0f;
result.area = Rect(result.minSize);
}
return *result;
}
/// Returns the default engine font. This font should not be freed.
@trusted
Font engineFont() {
return engineState.debugFont;
}
/// Returns the default filter mode.
Filter defaultFilter() {
return engineState.defaultFilter;
}
/// Returns the default wrap mode.
Wrap defaultWrap() {
return engineState.defaultWrap;
}
/// Sets the default filter mode to the specified value.
void setDefaultFilter(Filter value) {
engineState.defaultFilter = value;
}
/// Sets the default wrap mode to the specified value.
void setDefaultWrap(Wrap value) {
engineState.defaultWrap = value;
}
/// Returns the current master volume level.
@trusted
float masterVolume() {
return rl.GetMasterVolume();
}
/// Sets the master volume level to the specified value.
@trusted
void setMasterVolume(float value) {
rl.SetMasterVolume(value);
}
/// Returns true if the resolution is locked and cannot be changed.
bool isResolutionLocked() {
return !engineState.viewport.data.isEmpty;
}
/// Locks the resolution to the specified width and height.
@trusted
void lockResolution(int width, int height) {
engineState.viewport.lockWidth = width;
engineState.viewport.lockHeight = height;
if (engineState.flags & EngineFlag.isUpdating) {
engineState.viewport.isChanging = true;
} else {
engineState.viewport.data.resize(width, height);
engineViewportInfo(true);
}
}
/// Unlocks the resolution, allowing it to be changed.
void unlockResolution() {
if (engineState.flags & EngineFlag.isUpdating) {
engineState.viewport.isChanging = true;
} else {
auto temp = engineState.viewport.data.color;
engineState.viewport.data.free();
engineState.viewport.data.color = temp;
}
}
/// Toggles between the current resolution and the specified width and height.
void toggleResolution(int width, int height) {
if (isResolutionLocked) unlockResolution();
else lockResolution(width, height);
}
/// Returns the current screen width.
@trusted
int screenWidth() {
return rl.GetMonitorWidth(rl.GetCurrentMonitor());
}
/// Returns the current screen height.
@trusted
int screenHeight() {
return rl.GetMonitorHeight(rl.GetCurrentMonitor());
}
/// Returns the current screen size.
Vec2 screenSize() {
return Vec2(screenWidth, screenHeight);
}
/// Returns the current window width.
@trusted
int windowWidth() {
if (isFullscreen) return screenWidth;
else return rl.GetScreenWidth();
}
/// Returns the current window height.
@trusted
int windowHeight() {
if (isFullscreen) return screenHeight;
else return rl.GetScreenHeight();
}
/// Returns the current window size.
Vec2 windowSize() {
return Vec2(windowWidth, windowHeight);
}
/// Returns the current resolution width.
int resolutionWidth() {
if (isResolutionLocked) return engineState.viewport.data.width;
else return windowWidth;
}
/// Returns the current resolution height.
int resolutionHeight() {
if (isResolutionLocked) return engineState.viewport.data.height;
else return windowHeight;
}
/// Returns the current resolution size.
Vec2 resolution() {
return Vec2(resolutionWidth, resolutionHeight);
}
/// Returns the current position of the mouse on the screen.
@trusted
Vec2 mouse() {
return engineState.mouseBuffer;
}
/// Returns the current frames per second (FPS).
@trusted
int fps() {
return rl.GetFPS();
}
/// Returns the total elapsed time since the application started.
@trusted
double elapsedTime() {
return rl.GetTime();
}
/// Returns the total number of ticks elapsed since the application started.
long elapsedTickCount() {
return engineState.tickCount;
}
/// Returns the time elapsed since the last frame.
@trusted
float deltaTime() {
return rl.GetFrameTime();
}
/// Returns the change in mouse position since the last frame.
@trusted
Vec2 deltaMouse() {
return rl.GetMouseDelta().toParin();
}
/// Returns the change in mouse wheel position since the last frame.
@trusted
float deltaWheel() {
auto result = 0.0f;
version (WebAssembly) {
result = -rl.GetMouseWheelMove();
} version (OSX) {
result = -rl.GetMouseWheelMove();
} else {
result = rl.GetMouseWheelMove();
}
if (result < 0.0f) result = -1.0f;
else if (result > 0.0f) result = 1.0f;
else result = 0.0f;
return result;
}
/// Measures the size of the specified text when rendered with the given font and draw options.
@trusted
Vec2 measureTextSizeX(Font font, IStr text, DrawOptions options = DrawOptions(), TextOptions extraOptions = TextOptions()) {
if (font.isEmpty || text.length == 0) return Vec2();
auto lineCodepointCount = 0;
auto lineMaxCodepointCount = 0;
auto textWidth = 0;
auto textMaxWidth = 0;
auto textHeight = font.size;
auto textCodepointIndex = 0;
while (textCodepointIndex < text.length) {
lineCodepointCount += 1;
auto codepointByteCount = 0;
auto codepoint = rl.GetCodepointNext(&text[textCodepointIndex], &codepointByteCount);
auto glyphIndex = rl.GetGlyphIndex(font.data, codepoint);
if (codepoint != '\n') {
if (font.data.glyphs[glyphIndex].advanceX) {
textWidth += font.data.glyphs[glyphIndex].advanceX + font.runeSpacing;
} else {
textWidth += cast(int) (font.data.recs[glyphIndex].width + font.data.glyphs[glyphIndex].offsetX + font.runeSpacing);
}
} else {
if (textMaxWidth < textWidth) textMaxWidth = textWidth;
lineCodepointCount = 0;
textWidth = 0;
textHeight += font.lineSpacing;
}
if (lineMaxCodepointCount < lineCodepointCount) lineMaxCodepointCount = lineCodepointCount;
textCodepointIndex += codepointByteCount;
}
if (textMaxWidth < textWidth) textMaxWidth = textWidth;
if (textMaxWidth < extraOptions.alignmentWidth) textMaxWidth = extraOptions.alignmentWidth;
return Vec2(textMaxWidth * options.scale.x, textHeight * options.scale.y).floor();
}
/// Measures the size of the specified text when rendered with the given font and draw options.
Vec2 measureTextSize(FontId font, IStr text, DrawOptions options = DrawOptions()) {
return measureTextSizeX(font.getOr(), text, options);
}
/// Returns true if the specified key is currently pressed.
@trusted
bool isDown(char key) {
return rl.IsKeyDown(toUpper(key));
}
/// Returns true if the specified key is currently pressed.
@trusted
bool isDown(Keyboard key) {
if (key == Keyboard.shift) {
return rl.IsKeyDown(key) || rl.IsKeyDown(rl.KEY_RIGHT_SHIFT);
} else if (key == Keyboard.ctrl) {
return rl.IsKeyDown(key) || rl.IsKeyDown(rl.KEY_RIGHT_CONTROL);
} else if (key == Keyboard.alt) {
return rl.IsKeyDown(key) || rl.IsKeyDown(rl.KEY_RIGHT_ALT);
} else {
return rl.IsKeyDown(key);
}
}
/// Returns true if the specified key is currently pressed.
@trusted
bool isDown(Mouse key) {
if (key) return rl.IsMouseButtonDown(key - 1);
else return false;
}
/// Returns true if the specified key is currently pressed.
@trusted
bool isDown(Gamepad key, int id = 0) {
return rl.IsGamepadButtonDown(id, key);
}
/// Returns true if the specified key was pressed.
@trusted
bool isPressed(char key) {
return rl.IsKeyPressed(toUpper(key));
}
/// Returns true if the specified key was pressed.
@trusted
bool isPressed(Keyboard key) {
if (key == Keyboard.shift) {
return rl.IsKeyPressed(key) || rl.IsKeyPressed(rl.KEY_RIGHT_SHIFT);
} else if (key == Keyboard.ctrl) {
return rl.IsKeyPressed(key) || rl.IsKeyPressed(rl.KEY_RIGHT_CONTROL);
} else if (key == Keyboard.alt) {
return rl.IsKeyPressed(key) || rl.IsKeyPressed(rl.KEY_RIGHT_ALT);
} else {
return rl.IsKeyPressed(key);
}
}
/// Returns true if the specified key was pressed.
@trusted
bool isPressed(Mouse key) {
if (key) return rl.IsMouseButtonPressed(key - 1);
else return false;
}
/// Returns true if the specified key was pressed.
@trusted
bool isPressed(Gamepad key, int id = 0) {
return rl.IsGamepadButtonPressed(id, key);
}
/// Returns true if the specified key was released.
@trusted
bool isReleased(char key) {
return rl.IsKeyReleased(toUpper(key));
}
/// Returns true if the specified key was released.
@trusted
bool isReleased(Keyboard key) {
if (key == Keyboard.shift) {
return rl.IsKeyReleased(key) || rl.IsKeyReleased(rl.KEY_RIGHT_SHIFT);
} else if (key == Keyboard.ctrl) {
return rl.IsKeyReleased(key) || rl.IsKeyReleased(rl.KEY_RIGHT_CONTROL);
} else if (key == Keyboard.alt) {
return rl.IsKeyReleased(key) || rl.IsKeyReleased(rl.KEY_RIGHT_ALT);
} else {
return rl.IsKeyReleased(key);
}
}
/// Returns true if the specified key was released.
@trusted
bool isReleased(Mouse key) {
if (key) return rl.IsMouseButtonReleased(key - 1);
else return false;
}
/// Returns true if the specified key was released.
@trusted
bool isReleased(Gamepad key, int id = 0) {
return rl.IsGamepadButtonReleased(id, key);
}
/// Returns the recently pressed keyboard key.
/// This function acts like a queue, meaning that multiple calls will return other recently pressed keys.
/// A none key is returned when the queue is empty.
@trusted
Keyboard dequeuePressedKey() {
return cast(Keyboard) rl.GetKeyPressed();
}
/// Returns the recently pressed character.
/// This function acts like a queue, meaning that multiple calls will return other recently pressed characters.
/// A none character is returned when the queue is empty.
@trusted
dchar dequeuePressedRune() {
return rl.GetCharPressed();
}
/// Returns the directional input based on the WASD and arrow keys when they are down.
/// The vector is not normalized.
Vec2 wasd() {
with (Keyboard) return Vec2(
(d.isDown || right.isDown) - (a.isDown || left.isDown),
(s.isDown || down.isDown) - (w.isDown || up.isDown),
);
}
/// Returns the directional input based on the WASD and arrow keys when they are pressed.
/// The vector is not normalized.
Vec2 wasdPressed() {
with (Keyboard) return Vec2(
(d.isPressed || right.isPressed) - (a.isPressed || left.isPressed),
(s.isPressed || down.isPressed) - (w.isPressed || up.isPressed),
);
}
/// Returns the directional input based on the WASD and arrow keys when they are released.
/// The vector is not normalized.
Vec2 wasdReleased() {
with (Keyboard) return Vec2(
(d.isReleased || right.isReleased) - (a.isReleased || left.isReleased),
(s.isReleased || down.isReleased) - (w.isReleased || up.isReleased),
);
}
/// Plays the specified sound.
@trusted
void playSoundX(ref Sound sound) {
if (sound.isEmpty) return;
if (sound.isPaused) resumeSoundX(sound);
if (sound.data.isType!(rl.Sound)) {
rl.PlaySound(sound.data.get!(rl.Sound)());
} else {
rl.StopMusicStream(sound.data.get!(rl.Music)());
rl.PlayMusicStream(sound.data.get!(rl.Music)());
}
}
/// Plays the specified sound.
void playSound(SoundId sound) {
if (sound.isValid) playSoundX(sound.get());
}
/// Stops playback of the specified sound.
@trusted
void stopSoundX(ref Sound sound) {
if (sound.isEmpty) return;
if (sound.data.isType!(rl.Sound)) {
rl.StopSound(sound.data.get!(rl.Sound)());
} else {
rl.StopMusicStream(sound.data.get!(rl.Music)());
}
}
/// Stops playback of the specified sound.
void stopSound(SoundId sound) {
if (sound.isValid) stopSoundX(sound.get());
}
/// Pauses playback of the specified sound.
@trusted
void pauseSoundX(ref Sound sound) {
if (sound.isEmpty) return;
sound.isPaused = true;
if (sound.data.isType!(rl.Sound)) {
rl.PauseSound(sound.data.get!(rl.Sound)());
} else {
rl.PauseMusicStream(sound.data.get!(rl.Music)());
}
}
/// Pauses playback of the specified sound.
void pauseSound(SoundId sound) {
if (sound.isValid) pauseSoundX(sound.get());
}
/// Resumes playback of the specified paused sound.
@trusted
void resumeSoundX(ref Sound sound) {
if (sound.isEmpty) return;
sound.isPaused = false;
if (sound.data.isType!(rl.Sound)) {
rl.ResumeSound(sound.data.get!(rl.Sound)());
} else {
rl.ResumeMusicStream(sound.data.get!(rl.Music)());
}
}
/// Resumes playback of the specified paused sound.
void resumeSound(SoundId sound) {
if (sound.isValid) resumeSoundX(sound.get());
}
/// Updates the playback state of the specified sound.
@trusted
void updateSoundX(ref Sound sound) {
if (sound.isEmpty) return;
if (sound.data.isType!(rl.Sound)) {
if (sound.isLooping && !sound.isPlaying) playSoundX(sound);
} else {
if (!sound.isLooping && (sound.duration - sound.time) < 0.1f) stopSoundX(sound);
rl.UpdateMusicStream(sound.data.get!(rl.Music)());
}
}
/// Updates the playback state of the specified sound.
void updateSound(SoundId sound) {
if (sound.isValid) updateSoundX(sound.get());
}
/// Draws a rectangle with the specified area and color.
@trusted
void drawRect(Rect area, Color color = white) {
if (isPixelSnapped) {
rl.DrawRectanglePro(area.floor().toRl(), rl.Vector2(0.0f, 0.0f), 0.0f, color.toRl());
} else {
rl.DrawRectanglePro(area.toRl(), rl.Vector2(0.0f, 0.0f), 0.0f, color.toRl());
}
}
/// Draws a hollow rectangle with the specified area and color.
@trusted
void drawHollowRect(Rect area, float thickness, Color color = white) {
if (isPixelSnapped) {
rl.DrawRectangleLinesEx(area.floor().toRl(), thickness, color.toRl());
} else {
rl.DrawRectangleLinesEx(area.toRl(), thickness, color.toRl());
}
}
/// Draws a point at the specified location with the given size and color.
void drawVec2(Vec2 point, float size, Color color = white) {
drawRect(Rect(point, size, size).centerArea, color);
}
/// Draws a circle with the specified area and color.
@trusted
void drawCirc(Circ area, Color color = white) {
if (isPixelSnapped) {
rl.DrawCircleV(area.position.floor().toRl(), area.radius, color.toRl());
} else {
rl.DrawCircleV(area.position.toRl(), area.radius, color.toRl());
}
}
/// Draws a hollow circle with the specified area and color.
@trusted
void drawHollowCirc(Circ area, float thickness, Color color = white) {
if (isPixelSnapped) {
rl.DrawRing(area.position.floor().toRl(), area.radius - thickness, area.radius, 0.0f, 360.0f, 30, color.toRl());
} else {
rl.DrawRing(area.position.toRl(), area.radius - thickness, area.radius, 0.0f, 360.0f, 30, color.toRl());
}
}
/// Draws a line with the specified area, thickness, and color.
@trusted
void drawLine(Line area, float size, Color color = white) {
if (isPixelSnapped) {
rl.DrawLineEx(area.a.floor().toRl(), area.b.floor().toRl(), size, color.toRl());
} else {
rl.DrawLineEx(area.a.toRl(), area.b.toRl(), size, color.toRl());
}
}
/// Draws a portion of the specified texture at the given position with the specified draw options.
@trusted
void drawTextureAreaX(Texture texture, Rect area, Vec2 position, DrawOptions options = DrawOptions()) {
if (texture.isEmpty || area.size.x <= 0.0f || area.size.y <= 0.0f) return;
auto target = Rect(position, area.size * options.scale.abs());
auto origin = options.origin.isZero ? target.origin(options.hook) : options.origin;
auto flip = options.flip;
if (options.scale.x < 0.0f && options.scale.y < 0.0f) {
flip = oppositeFlip(flip, Flip.xy);
} else if (options.scale.x < 0.0f) {
flip = oppositeFlip(flip, Flip.x);
} else if (options.scale.y < 0.0f) {
flip = oppositeFlip(flip, Flip.y);
}
final switch (flip) {
case Flip.none: break;
case Flip.x: area.size.x *= -1.0f; break;
case Flip.y: area.size.y *= -1.0f; break;
case Flip.xy: area.size *= Vec2(-1.0f); break;
}
if (isPixelSnapped) {
rl.DrawTexturePro(
texture.data,
area.floor().toRl(),
target.floor().toRl(),
origin.floor().toRl(),
options.rotation,
options.color.toRl(),
);
} else {
rl.DrawTexturePro(
texture.data,
area.toRl(),
target.toRl(),
origin.toRl(),
options.rotation,
options.color.toRl(),
);
}
}
/// Draws a portion of the specified texture at the given position with the specified draw options.
void drawTextureArea(TextureId texture, Rect area, Vec2 position, DrawOptions options = DrawOptions()) {
drawTextureAreaX(texture.getOr(), area, position, options);
}
/// Draws the texture at the given position with the specified draw options.
void drawTextureX(Texture texture, Vec2 position, DrawOptions options = DrawOptions()) {
drawTextureAreaX(texture, Rect(texture.size), position, options);
}
/// Draws the texture at the given position with the specified draw options.
void drawTexture(TextureId texture, Vec2 position, DrawOptions options = DrawOptions()) {
drawTextureX(texture.getOr(), position, options);
}
/// Draws a 9-patch texture from the specified texture area at the given target area.
void drawTexturePatchX(Texture texture, Rect area, Rect target, bool isTiled, DrawOptions options = DrawOptions()) {
auto tileSize = (area.size / Vec2(3.0f)).floor();
auto hOptions = options;
auto vOptions = options;
auto cOptions = options;
auto cleanScaleX = (target.size.x - 2.0f * tileSize.x) / tileSize.x;
auto cleanScaleY = (target.size.y - 2.0f * tileSize.y) / tileSize.y;
hOptions.scale.x *= cleanScaleX;
vOptions.scale.y *= cleanScaleY;
cOptions.scale = Vec2(hOptions.scale.x, vOptions.scale.y);
// 1
auto partPosition = target.position;
auto partArea = Rect(area.position, tileSize);
drawTextureAreaX(texture, partArea, partPosition, options);
// 2
partPosition.x += tileSize.x * options.scale.x;
partArea.position.x += tileSize.x;
if (isTiled) {
foreach (i; 0 .. cast(int) cleanScaleX.ceil()) {
auto tempPartPosition = partPosition;
tempPartPosition.x += i * tileSize.x * options.scale.x;
drawTextureAreaX(texture, partArea, tempPartPosition, options);
}
} else {
drawTextureAreaX(texture, partArea, partPosition, hOptions);
}
// 3
partPosition.x += tileSize.x * hOptions.scale.x;
partArea.position.x += tileSize.x;
drawTextureAreaX(texture, partArea, partPosition, options);
// 4
partPosition.x = target.position.x;
partPosition.y += tileSize.y * options.scale.y;
partArea.position.x = area.position.x;
partArea.position.y += tileSize.y;
if (isTiled) {
foreach (i; 0 .. cast(int) cleanScaleY.ceil()) {
auto tempPartPosition = partPosition;
tempPartPosition.y += i * tileSize.y * options.scale.y;
drawTextureAreaX(texture, partArea, tempPartPosition, options);
}
} else {
drawTextureAreaX(texture, partArea, partPosition, vOptions);
}
// 5
partPosition.x += tileSize.x * options.scale.x;
partArea.position.x += tileSize.x;
drawTextureAreaX(texture, partArea, partPosition, cOptions);
// 6
partPosition.x += tileSize.x * hOptions.scale.x;
partArea.position.x += tileSize.x;
if (isTiled) {
foreach (i; 0 .. cast(int) cleanScaleY.ceil()) {
auto tempPartPosition = partPosition;
tempPartPosition.y += i * tileSize.y * options.scale.y;
drawTextureAreaX(texture, partArea, tempPartPosition, options);
}
} else {
drawTextureAreaX(texture, partArea, partPosition, vOptions);
}
// 7
partPosition.x = target.position.x;
partPosition.y += tileSize.y * vOptions.scale.y;
partArea.position.x = area.position.x;
partArea.position.y += tileSize.y;
drawTextureAreaX(texture, partArea, partPosition, options);
// 8
partPosition.x += tileSize.x * options.scale.x;
partArea.position.x += tileSize.x;
if (isTiled) {
foreach (i; 0 .. cast(int) cleanScaleX.ceil()) {
auto tempPartPosition = partPosition;
tempPartPosition.x += i * tileSize.x * options.scale.x;
drawTextureAreaX(texture, partArea, tempPartPosition, options);
}
} else {
drawTextureAreaX(texture, partArea, partPosition, hOptions);
}
// 9
partPosition.x += tileSize.x * hOptions.scale.x;
partArea.position.x += tileSize.x;
drawTextureAreaX(texture, partArea, partPosition, options);
}
/// Draws a 9-patch texture from the specified texture area at the given target area.
void drawTexturePatch(TextureId texture, Rect area, Rect target, bool isTiled, DrawOptions options = DrawOptions()) {
drawTexturePatchX(texture.getOr(), area, target, isTiled, options);
}
/// Draws a portion of the specified viewport at the given position with the specified draw options.
void drawViewportArea(Viewport viewport, Rect area, Vec2 position, DrawOptions options = DrawOptions()) {
// Some basic rules to make viewports noob friendly.
final switch (options.flip) {
case Flip.none: options.flip = Flip.y; break;
case Flip.x: options.flip = Flip.xy; break;
case Flip.y: options.flip = Flip.none; break;
case Flip.xy: options.flip = Flip.x; break;
}
drawTextureAreaX(viewport.data.texture.toParin(), area, position, options);
}
/// Draws the viewport at the given position with the specified draw options.
void drawViewport(Viewport viewport, Vec2 position, DrawOptions options = DrawOptions()) {
drawViewportArea(viewport, Rect(viewport.size), position, options);
}
/// Draws a single character from the specified font at the given position with the specified draw options.
@trusted
void drawRuneX(Font font, dchar rune, Vec2 position, DrawOptions options = DrawOptions()) {
if (font.isEmpty) return;
auto rect = toParin(rl.GetGlyphAtlasRec(font.data, rune));
auto origin = options.origin.isZero ? rect.origin(options.hook) : options.origin;
rl.rlPushMatrix();
if (isPixelSnapped) {
rl.rlTranslatef(position.x.floor(), position.y.floor(), 0.0f);
} else {
rl.rlTranslatef(position.x, position.y, 0.0f);
}
rl.rlRotatef(options.rotation, 0.0f, 0.0f, 1.0f);
rl.rlScalef(options.scale.x, options.scale.y, 1.0f);
rl.rlTranslatef(-origin.x.floor(), -origin.y.floor(), 0.0f);
rl.DrawTextCodepoint(font.data, rune, rl.Vector2(0.0f, 0.0f), font.size, options.color.toRl());
rl.rlPopMatrix();
}
/// Draws a single character from the specified font at the given position with the specified draw options.
void drawRune(FontId font, dchar rune, Vec2 position, DrawOptions options = DrawOptions()) {
drawRuneX(font.getOr(), rune, position, options);
}
/// Draws the specified text with the given font at the given position using the provided draw options.
// NOTE: Text drawing needs to go over the text 3 times. This can be made into 2 times in the future if needed by copy-pasting the measureTextSize inside this function.
@trusted
void drawTextX(Font font, IStr text, Vec2 position, DrawOptions options = DrawOptions(), TextOptions extraOptions = TextOptions()) {
static FixedList!(IStr, 128) linesBuffer = void;
static FixedList!(short, 128) linesWidthBuffer = void;
if (font.isEmpty || text.length == 0) return;
linesBuffer.clear();
linesWidthBuffer.clear();
// Get some info about the text.
auto textCodepointCount = 0;
auto textMaxLineWidth = 0;
auto textHeight = font.size;
{
auto lineCodepointIndex = 0;
auto textCodepointIndex = 0;
while (textCodepointIndex < text.length) {
textCodepointCount += 1;
auto codepointSize = 0;
auto codepoint = rl.GetCodepointNext(&text[textCodepointIndex], &codepointSize);
if (codepoint == '\n' || textCodepointIndex == text.length - codepointSize) {
linesBuffer.append(text[lineCodepointIndex .. textCodepointIndex + (codepoint != '\n')]);
linesWidthBuffer.append(cast(ushort) (measureTextSizeX(font, linesBuffer[$ - 1]).x));
if (textMaxLineWidth < linesWidthBuffer[$ - 1]) textMaxLineWidth = linesWidthBuffer[$ - 1];
if (codepoint == '\n') textHeight += font.lineSpacing;
lineCodepointIndex = cast(ushort) (textCodepointIndex + 1);
}
textCodepointIndex += codepointSize;
}
if (textMaxLineWidth < extraOptions.alignmentWidth) textMaxLineWidth = extraOptions.alignmentWidth;
}
// Prepare the the text for drawing.
auto origin = Rect(textMaxLineWidth, textHeight).origin(options.hook);
rl.rlPushMatrix();
if (isPixelSnapped) {
rl.rlTranslatef(position.x.floor(), position.y.floor(), 0.0f);
} else {
rl.rlTranslatef(position.x, position.y, 0.0f);
}
rl.rlRotatef(options.rotation, 0.0f, 0.0f, 1.0f);
rl.rlScalef(options.scale.x, options.scale.y, 1.0f);
rl.rlTranslatef(-origin.x.floor(), -origin.y.floor(), 0.0f);
// Draw the text.
auto drawMaxCodepointCount = extraOptions.visibilityCount
? extraOptions.visibilityCount
: textCodepointCount * extraOptions.visibilityRatio;
auto drawCodepointCounter = 0;
auto textOffsetY = 0;
foreach (i, line; linesBuffer) {
auto lineCodepointIndex = 0;
// Find the initial x offset for the text.
auto textOffsetX = 0;
if (extraOptions.isRightToLeft) {
final switch (extraOptions.alignment) {
case Alignment.left: textOffsetX = linesWidthBuffer[i]; break;
case Alignment.center: textOffsetX = textMaxLineWidth / 2 + linesWidthBuffer[i] / 2; break;
case Alignment.right: textOffsetX = textMaxLineWidth; break;
}
} else {
final switch (extraOptions.alignment) {
case Alignment.left: break;
case Alignment.center: textOffsetX = textMaxLineWidth / 2 - linesWidthBuffer[i] / 2; break;
case Alignment.right: textOffsetX = textMaxLineWidth - linesWidthBuffer[i]; break;
}
}
// Go over the characters and draw them.
if (extraOptions.isRightToLeft) {
lineCodepointIndex = cast(int) line.length;
while (lineCodepointIndex > 0) {
if (drawCodepointCounter >= drawMaxCodepointCount) break;
auto codepointSize = 0;
auto codepoint = rl.GetCodepointPrevious(&line.ptr[lineCodepointIndex], &codepointSize);
auto glyphIndex = rl.GetGlyphIndex(font.data, codepoint);
if (lineCodepointIndex == line.length) {
if (font.data.glyphs[glyphIndex].advanceX) {
textOffsetX -= font.data.glyphs[glyphIndex].advanceX + font.runeSpacing;
} else {
textOffsetX -= cast(int) (font.data.recs[glyphIndex].width + font.runeSpacing);
}
} else {
auto temp = 0;
auto nextRightToLeftGlyphIndex = rl.GetGlyphIndex(font.data, rl.GetCodepointPrevious(&line[lineCodepointIndex], &temp));
if (font.data.glyphs[nextRightToLeftGlyphIndex].advanceX) {
textOffsetX -= font.data.glyphs[nextRightToLeftGlyphIndex].advanceX + font.runeSpacing;
} else {
textOffsetX -= cast(int) (font.data.recs[nextRightToLeftGlyphIndex].width + font.runeSpacing);
}
}
if (codepoint != ' ' && codepoint != '\t') {
rl.DrawTextCodepoint(font.data, codepoint, rl.Vector2(textOffsetX, textOffsetY), font.size, options.color.toRl());
}
drawCodepointCounter += 1;
lineCodepointIndex -= codepointSize;
}
drawCodepointCounter += 1;
textOffsetY += font.lineSpacing;
} else {
while (lineCodepointIndex < line.length) {
if (drawCodepointCounter >= drawMaxCodepointCount) break;
auto codepointSize = 0;
auto codepoint = rl.GetCodepointNext(&line[lineCodepointIndex], &codepointSize);
auto glyphIndex = rl.GetGlyphIndex(font.data, codepoint);
if (codepoint != ' ' && codepoint != '\t') {
rl.DrawTextCodepoint(font.data, codepoint, rl.Vector2(textOffsetX, textOffsetY), font.size, options.color.toRl());
}
if (font.data.glyphs[glyphIndex].advanceX) {
textOffsetX += font.data.glyphs[glyphIndex].advanceX + font.runeSpacing;
} else {
textOffsetX += cast(int) (font.data.recs[glyphIndex].width + font.runeSpacing);
}
drawCodepointCounter += 1;
lineCodepointIndex += codepointSize;
}
drawCodepointCounter += 1;
textOffsetY += font.lineSpacing;
}
}
rl.rlPopMatrix();
}
/// Draws text with the given font at the given position using the provided draw options.
void drawText(FontId font, IStr text, Vec2 position, DrawOptions options = DrawOptions(), TextOptions extraOptions = TextOptions()) {
drawTextX(font.getOr(), text, position, options, extraOptions);
}
/// Draws debug text at the given position with the provided draw options.
void drawDebugText(IStr text, Vec2 position, DrawOptions options = DrawOptions(), TextOptions extraOptions = TextOptions()) {
drawTextX(engineFont, text, position, options, extraOptions);
}
/// Mixes in a game loop template with specified functions for initialization, update, and cleanup, and sets window size and title.
mixin template runGame(alias readyFunc, alias updateFunc, alias finishFunc, int width = 960, int height = 540, IStr title = "Parin") {
version (D_BetterC) {
extern(C)
void main(int argc, immutable(char)** argv) {
openWindow(width, height, null, title);
openWindowExtraStep(argc, argv);
readyFunc();
updateWindow(&updateFunc);
finishFunc();
closeWindow();
}
} else {
void main(immutable(char)[][] args) {
openWindow(width, height, args, title);
readyFunc();
updateWindow(&updateFunc);
finishFunc();
closeWindow();
}
}
}