diff options
Diffstat (limited to 'game')
| -rw-r--r-- | game/CMakeLists.txt | 9 | ||||
| -rw-r--r-- | game/Config.h | 21 | ||||
| -rw-r--r-- | game/GameScene.cpp | 58 | ||||
| -rw-r--r-- | game/Random.cpp | 5 | ||||
| -rw-r--r-- | game/Random.h | 2 | ||||
| -rw-r--r-- | game/background/CMakeLists.txt | 9 | ||||
| -rw-r--r-- | game/main.cpp | 9 | ||||
| -rw-r--r-- | game/menus/MenusConfig.h | 1 | ||||
| -rw-r--r-- | game/menus/mainmenu/ITransitionScript.cpp | 1 | ||||
| -rw-r--r-- | game/menus/shop/ShopMenuScene.cpp | 2 | ||||
| -rw-r--r-- | game/prefab/CMakeLists.txt | 6 | ||||
| -rw-r--r-- | game/prefab/ZapperObject.cpp | 122 | ||||
| -rw-r--r-- | game/prefab/ZapperObject.h | 40 | ||||
| -rw-r--r-- | game/prefab/ZapperPoolScript.cpp | 73 | ||||
| -rw-r--r-- | game/prefab/ZapperPoolScript.h | 33 | ||||
| -rw-r--r-- | game/prefab/ZapperPoolSubScene.cpp | 19 | ||||
| -rw-r--r-- | game/prefab/ZapperPoolSubScene.h | 19 | ||||
| -rwxr-xr-x | game/util/scrollgen | 36 | 
18 files changed, 408 insertions, 57 deletions
| diff --git a/game/CMakeLists.txt b/game/CMakeLists.txt index 4e31f80..4d02633 100644 --- a/game/CMakeLists.txt +++ b/game/CMakeLists.txt @@ -7,7 +7,10 @@ set(CMAKE_BUILD_TYPE Debug)  project(game C CXX)  add_subdirectory(../src crepe) -add_executable(main + +add_executable(main) + +target_sources(main PUBLIC  	background/AquariumSubScene.cpp  	background/AquariumScript.cpp  	background/BackgroundSubScene.cpp @@ -53,5 +56,9 @@ add_executable(main  	Random.cpp  ) +add_subdirectory(background) +add_subdirectory(prefab) +  target_link_libraries(main PUBLIC crepe) +target_include_directories(main PRIVATE .) diff --git a/game/Config.h b/game/Config.h index f1b64b3..64f2828 100644 --- a/game/Config.h +++ b/game/Config.h @@ -1,6 +1,17 @@  #pragma once  #include "types.h" +#include <crepe/api/Config.h> + +static const crepe::Config ENGINE_CONFIG { +	.log { +		.level = crepe::Log::Level::DEBUG, +	}, +	.window_settings { +		.window_title = "Jetpack joyride clone", +	}, +}; +  static constexpr int SORT_IN_LAY_BACK_BACKGROUND = 3; // For all scenes  static constexpr int SORT_IN_LAY_BACKGROUND = 4; // For all scenes  static constexpr int SORT_IN_LAY_FORE_BACKGROUND = 5; // For all scenes @@ -21,11 +32,12 @@ static constexpr int COLL_LAY_ZAPPER = 6; // Only for GameScene  static constexpr int COLL_LAY_LASER = 7; // Only for GameScene  static constexpr int COLL_LAY_MISSILE = 8; // Only for GameScene -static constexpr int GAME_HEIGHT = 800; // In game units +static constexpr float GAME_HEIGHT = 800; // In game units +static constexpr float HALLWAY_HEIGHT = 475; // In game units -static constexpr int VIEWPORT_X = 1100; // In game units +static constexpr float VIEWPORT_X = 1100; // In game units  // 'GAME_HEIGHT' (below) should be replaced by '500' when game development is finished -static constexpr int VIEWPORT_Y = 500; // In game units +static constexpr float VIEWPORT_Y = 500; // In game units  // Font settings  static constexpr const char * FONT = "Jetpackia"; @@ -45,3 +57,6 @@ static constexpr const char * DISTANCE_RUN = "distance_run";  static constexpr const char * PLAYER_NAME = "player";  static constexpr int PLAYER_SPEED = 7500; // In game units  static constexpr int PLAYER_GRAVITY_SCALE = 60; // In game units + +static constexpr const char* CAMERA_NAME = "camera"; + diff --git a/game/GameScene.cpp b/game/GameScene.cpp index c84ec24..9de2fd1 100644 --- a/game/GameScene.cpp +++ b/game/GameScene.cpp @@ -11,6 +11,7 @@  #include "hud/SpeedScript.h"  #include "menus/endgame/EndGameSubScene.h"  #include "player/PlayerSubScene.h" +#include "prefab/ZapperPoolSubScene.h"  #include "workers/WorkersSubScene.h"  #include <cmath> @@ -34,9 +35,11 @@ using namespace crepe;  using namespace std;  void GameScene::load_scene() { +	logf(Log::DEBUG, "Loading (main) GameScene..."); +  	BackgroundSubScene background(*this); -	GameObject camera = new_object("camera", "camera", vec2(650, 0)); +	GameObject camera = new_object(CAMERA_NAME, "camera", vec2(650, 0));  	camera.add_component<Camera>(  		ivec2(990, 720), vec2(VIEWPORT_X, VIEWPORT_Y),  		Camera::Data { @@ -78,6 +81,8 @@ void GameScene::load_scene() {  	});  	ceiling.add_component<BoxCollider>(vec2(INFINITY, 200)); +	ZapperPoolSubScene {*this}; +  	GameObject start_game_script = new_object("start_game_script", "script", vec2(0, 0));  	start_game_script.add_component<BehaviorScript>().set_script<StartGameScript>(); @@ -87,6 +92,7 @@ void GameScene::load_scene() {  	HudSubScene hud;  	hud.create(*this); +  	GameObject background_music = new_object("background_music", "audio", vec2(0, 0));  	Asset background_music_asset {"asset/music/level.ogg"};  	background_music.add_component<AudioSource>(background_music_asset); @@ -95,56 +101,6 @@ void GameScene::load_scene() {  	Asset boom_audio_asset {"asset/sfx/window_smash.ogg"};  	boom_audio.add_component<AudioSource>(boom_audio_asset); -	// zapper, laser and missile (below) for testing purpose only!!! -	GameObject zapper = new_object("zapper", "zapper", vec2(1000, 200)); -	Asset zapper_asset {"asset/obstacles/zapper/regular_zappers/zapEffect.png"}; -	Sprite & zapper_sprite = zapper.add_component<Sprite>( -		zapper_asset, -		Sprite::Data { -			.sorting_in_layer = SORT_IN_LAY_OBSTACLES, -			.order_in_layer = 0, -			.size = vec2(100, 100), -		} -	); -	zapper.add_component<Rigidbody>(Rigidbody::Data { -		.body_type = Rigidbody::BodyType::KINEMATIC, -		.kinematic_collision = false, -		.collision_layer = COLL_LAY_ZAPPER, -	}); -	zapper.add_component<BoxCollider>(vec2(100, 100)); -	GameObject laser = new_object("laser", "laser", vec2(2000, 200)); -	Asset laser_asset {"asset/obstacles/laser/laserPower.png"}; -	Sprite & laser_sprite = laser.add_component<Sprite>( -		laser_asset, -		Sprite::Data { -			.sorting_in_layer = SORT_IN_LAY_OBSTACLES, -			.order_in_layer = 0, -			.size = vec2(100, 100), -		} -	); -	laser.add_component<Rigidbody>(Rigidbody::Data { -		.body_type = Rigidbody::BodyType::KINEMATIC, -		.kinematic_collision = false, -		.collision_layer = COLL_LAY_LASER, -	}); -	laser.add_component<BoxCollider>(vec2(100, 100)); -	GameObject missile = new_object("missile", "missile", vec2(4000, 200)); -	Asset missile_asset {"asset/obstacles/missile/missile.png"}; -	Sprite & missile_sprite = missile.add_component<Sprite>( -		missile_asset, -		Sprite::Data { -			.sorting_in_layer = SORT_IN_LAY_OBSTACLES, -			.order_in_layer = 0, -			.size = vec2(100, 100), -		} -	); -	missile.add_component<Rigidbody>(Rigidbody::Data { -		.body_type = Rigidbody::BodyType::KINEMATIC, -		.kinematic_collision = false, -		.collision_layer = COLL_LAY_MISSILE, -	}); -	missile.add_component<BoxCollider>(vec2(100, 100)); -  	EndGameSubScene endgamewindow;  	endgamewindow.create(*this);  } diff --git a/game/Random.cpp b/game/Random.cpp index ace6245..821ddc8 100644 --- a/game/Random.cpp +++ b/game/Random.cpp @@ -25,3 +25,8 @@ unsigned Random::u(unsigned upper, unsigned lower) {  	unsigned x = rand() % range;  	return x + lower;  } + +bool Random::b() { +	return rand() % 2; +} + diff --git a/game/Random.h b/game/Random.h index 8af9669..4a1108a 100644 --- a/game/Random.h +++ b/game/Random.h @@ -6,4 +6,6 @@ public:  	static double d(double upper = 1.0, double lower = 0.0);  	static int i(int upper, int lower = 0);  	static unsigned u(unsigned upper, unsigned lower = 0); +	static bool b();  }; + diff --git a/game/background/CMakeLists.txt b/game/background/CMakeLists.txt new file mode 100644 index 0000000..1d705f5 --- /dev/null +++ b/game/background/CMakeLists.txt @@ -0,0 +1,9 @@ +target_sources(main PUBLIC +	AquariumSubScene.cpp +	BackgroundSubScene.cpp +	ForestParallaxScript.cpp +	ForestSubScene.cpp +	HallwaySubScene.cpp +	StartSubScene.cpp +) + diff --git a/game/main.cpp b/game/main.cpp index e341353..b6458b8 100644 --- a/game/main.cpp +++ b/game/main.cpp @@ -1,6 +1,9 @@ +#include <cstdlib> +  #include <crepe/api/Engine.h>  #include <crepe/api/Script.h> +#include "Config.h"  #include "GameScene.h"  #include "menus/mainmenu/MainMenuScene.h"  #include "menus/shop/ShopMenuScene.h" @@ -8,10 +11,14 @@  using namespace crepe;  int main() { +	srand(time(NULL)); + +	Config::get_instance() = ENGINE_CONFIG; +  	Engine gameloop; +	gameloop.add_scene<GameScene>();  	gameloop.add_scene<MainMenuScene>();  	gameloop.add_scene<ShopMenuScene>(); -	gameloop.add_scene<GameScene>();  	return gameloop.main();  } diff --git a/game/menus/MenusConfig.h b/game/menus/MenusConfig.h index 6ec5689..24b60e8 100644 --- a/game/menus/MenusConfig.h +++ b/game/menus/MenusConfig.h @@ -3,7 +3,6 @@  //generic menu config  static constexpr int STARTING_SORTING_IN_LAYER = 7; -static constexpr const char * CAMERA_NAME = "camera";  //Scene names  static constexpr const char * START_SCENE = "scene1";  static constexpr const char * PREVIEW_SCENE = "scene1"; diff --git a/game/menus/mainmenu/ITransitionScript.cpp b/game/menus/mainmenu/ITransitionScript.cpp index cd929a0..3e51a90 100644 --- a/game/menus/mainmenu/ITransitionScript.cpp +++ b/game/menus/mainmenu/ITransitionScript.cpp @@ -2,6 +2,7 @@  #include "MainMenuConfig.h"  #include "../MenusConfig.h" +#include "../../Config.h"  #include <crepe/api/Camera.h>  #include <crepe/api/Transform.h> diff --git a/game/menus/shop/ShopMenuScene.cpp b/game/menus/shop/ShopMenuScene.cpp index 21e3f02..f4d5e76 100644 --- a/game/menus/shop/ShopMenuScene.cpp +++ b/game/menus/shop/ShopMenuScene.cpp @@ -4,6 +4,8 @@  #include "../BannerSubScene.h"  #include "../ButtonSubScene.h"  #include "../MenusConfig.h" +#include "../BannerSubScene.h" +#include "../../Config.h"  #include <crepe/api/Camera.h>  #include <crepe/api/Sprite.h> diff --git a/game/prefab/CMakeLists.txt b/game/prefab/CMakeLists.txt new file mode 100644 index 0000000..6c36ef2 --- /dev/null +++ b/game/prefab/CMakeLists.txt @@ -0,0 +1,6 @@ +target_sources(main PUBLIC +	ZapperObject.cpp +	ZapperPoolSubScene.cpp +	ZapperPoolScript.cpp +) + diff --git a/game/prefab/ZapperObject.cpp b/game/prefab/ZapperObject.cpp new file mode 100644 index 0000000..0a290e0 --- /dev/null +++ b/game/prefab/ZapperObject.cpp @@ -0,0 +1,122 @@ +#include <crepe/api/Transform.h> + +#include "Config.h" +#include "ZapperObject.h" + +using namespace crepe; + +ZapperObject::ZapperObject(crepe::GameObject && base) +	: GameObject(std::move(base)), +	  sprite { +		  .orb_start = add_component<Sprite>( +			  Asset {"asset/obstacles/zapper/orbAnim.png"}, +			  Sprite::Data { +				  .sorting_in_layer = SORT_IN_LAY_OBSTACLES, +				  .order_in_layer = 1, +				  .size = vec2(0, 50) * SCALE, +			  } +		  ), +		  .orb_end = add_component<Sprite>( +			  sprite.orb_start.source, +			  Sprite::Data { +				  .flip = {true, true}, +				  .sorting_in_layer = SORT_IN_LAY_OBSTACLES, +				  .order_in_layer = 1, +				  .size = vec2(0, 50) * SCALE, +			  } +		  ), +		  .glow_start = add_component<Sprite>( +			  Asset {"asset/obstacles/zapper/regular_zappers/glow.png"}, +			  Sprite::Data { +				  .sorting_in_layer = SORT_IN_LAY_OBSTACLES, +				  .order_in_layer = -1, +				  .size = vec2(128, 128) * SCALE, +			  } +		  ), +		  .glow_end = add_component<Sprite>( +			  sprite.glow_start.source, +			  Sprite::Data { +				  .flip = {true, true}, +				  .sorting_in_layer = SORT_IN_LAY_OBSTACLES, +				  .order_in_layer = -1, +				  .size = vec2(128, 128) * SCALE, +			  } +		  ), +		  .beam = add_component<Sprite>( +			  Asset {"asset/obstacles/zapper/regular_zappers/zapEffect.png"}, +			  Sprite::Data { +				  .sorting_in_layer = SORT_IN_LAY_OBSTACLES, +				  .order_in_layer = 0, +				  .size = vec2(0, 40 * SCALE), +				  .angle_offset = 90, +			  } +		  ), +	  }, +	  animator { +		  .orb_start = add_component<Animator>( +			  sprite.orb_start, ivec2(62, 42), uvec2(4, 1), +			  Animator::Data { +				  .fps = 10, +				  .looping = true, +			  } +		  ), +		  .orb_end = add_component<Animator>( +			  sprite.orb_end, ivec2(62, 42), uvec2(4, 1), animator.orb_start.data +		  ), +		  .glow_start = add_component<Animator>( +			  sprite.glow_start, ivec2(128, 128), uvec2(16, 1), +			  Animator::Data { +				  .fps = 30, +				  .looping = true, +			  } +		  ), +		  .glow_end = add_component<Animator>( +			  sprite.glow_end, ivec2(128, 128), uvec2(16, 1), animator.glow_start.data +		  ), +	  }, +	  body {add_component<Rigidbody>(Rigidbody::Data { +		  .body_type = Rigidbody::BodyType::KINEMATIC, +		  .kinematic_collision = false, +		  .collision_layer = COLL_LAY_ZAPPER, +	  })}, +	  collider {add_component<BoxCollider>(vec2(0, 0))} { +	this->set_active(false); +	Log::logf(Log::DEBUG, "Creating zapper"); +} + +void ZapperObject::place(const crepe::vec2 & position, float rotation, float length) { +	Log::logf(Log::DEBUG, "Placing zapper [position = {}, rotation = {}, length = {}]", position, rotation, length); + +	this->transform.position = position; +	this->transform.rotation = rotation; + +	vec2 offset = vec2(0, 1) * length / 2; + +	this->sprite.orb_start.data.position_offset = offset; +	this->sprite.glow_start.data.position_offset = offset; +	this->sprite.orb_end.data.position_offset = -offset; +	this->sprite.glow_end.data.position_offset = -offset; + +	this->sprite.beam.data.size.x = length; + +	this->collider.dimensions = offset.rotate(rotation) * 2 + vec2(30, 30) * SCALE; +} + +void ZapperObject::set_active(bool active) { +	this->sprite.orb_start.active = active; +	this->sprite.orb_end.active = active; +	this->sprite.glow_start.active = active; +	this->sprite.glow_end.active = active; +	this->sprite.beam.active = active; + +	this->animator.orb_start.active = active; +	this->animator.orb_end.active = active; +	this->animator.glow_start.active = active; +	this->animator.glow_end.active = active; + +	this->body.active = active; +	this->collider.active = active; + +	this->active = active; +} + diff --git a/game/prefab/ZapperObject.h b/game/prefab/ZapperObject.h new file mode 100644 index 0000000..42edc51 --- /dev/null +++ b/game/prefab/ZapperObject.h @@ -0,0 +1,40 @@ +#pragma once + +#include <crepe/api/Animator.h> +#include <crepe/api/BoxCollider.h> +#include <crepe/api/GameObject.h> +#include <crepe/api/Rigidbody.h> +#include <crepe/api/Sprite.h> + +class ZapperObject : public crepe::GameObject { +public: +	ZapperObject(crepe::GameObject &&); + +public: +	bool active = true; + +	struct { +		crepe::Sprite & orb_start; +		crepe::Sprite & orb_end; +		crepe::Sprite & glow_start; +		crepe::Sprite & glow_end; +		crepe::Sprite & beam; +	} sprite; + +	struct { +		crepe::Animator & orb_start; +		crepe::Animator & orb_end; +		crepe::Animator & glow_start; +		crepe::Animator & glow_end; +	} animator; + +	crepe::Rigidbody & body; +	crepe::BoxCollider & collider; + +private: +	static constexpr float SCALE = 0.8; + +public: +	void place(const crepe::vec2 & position, float rotation, float length); +	void set_active(bool active); +}; diff --git a/game/prefab/ZapperPoolScript.cpp b/game/prefab/ZapperPoolScript.cpp new file mode 100644 index 0000000..ac6ce96 --- /dev/null +++ b/game/prefab/ZapperPoolScript.cpp @@ -0,0 +1,73 @@ +#include <crepe/api/Camera.h> + +#include "../Config.h" +#include "../Random.h" + +#include "ZapperPoolScript.h" +#include "ZapperPoolSubScene.h" +#include "util/OptionalRef.h" + +using namespace crepe; +using namespace std; + +ZapperPoolScript::ZapperPoolScript(std::vector<ZapperObject> && pool) : pool(pool) {} + +void ZapperPoolScript::init() { +	subscribe<CreateZapperEvent>([this](const CreateZapperEvent &) { +		this->spawn_random(); +		return true; +	}); + +	camera_transform = get_components_by_name<Transform>(CAMERA_NAME).back(); +	camera_camera = get_components_by_name<Camera>(CAMERA_NAME).back(); +} + +void ZapperPoolScript::fixed_update(crepe::duration_t) { +	float threshold = camera_transform->position.x - camera_camera->viewport_size.x / 2 - OFFSCREEN_MARGIN; +	for (ZapperObject & zapper : this->pool) { +		if (!zapper.active) continue; + +		if (zapper.transform.position.x < threshold) +			zapper.set_active(false); +	} + +	if (i-- > 0) return; +	i = 200; +	queue_event<CreateZapperEvent>(); +} + +void ZapperPoolScript::spawn_random() { +	OptionalRef<ZapperObject> zapper = this->get_next_zapper(); +	if (!zapper) return; // pool exhausted +	 +	vec2 pos = { +		.x = camera_transform->position.x + camera_camera->viewport_size.x / 2 + OFFSCREEN_MARGIN, +		.y = Random::f(0.5, -0.5) * HALLWAY_HEIGHT, +	}; + +	bool horizontal = Random::b(); +	float rotation, length; + +	if (horizontal) { +		rotation = 90; +		length = Random::f(400, 200); +	} else { +		rotation = 0; +		length = Random::f(200, 50); +		if (abs(pos.y) + length / 2 > HALLWAY_HEIGHT / 2) { +			// TODO: fix offset +		} +	} + +	zapper->place(pos, rotation, length); +	zapper->set_active(true); +} + +OptionalRef<ZapperObject> ZapperPoolScript::get_next_zapper() { +	for (ZapperObject & zapper : this->pool) { +		if (zapper.active) continue; +		return zapper; +	} +	return {}; +} + diff --git a/game/prefab/ZapperPoolScript.h b/game/prefab/ZapperPoolScript.h new file mode 100644 index 0000000..6aee8b2 --- /dev/null +++ b/game/prefab/ZapperPoolScript.h @@ -0,0 +1,33 @@ +#pragma once + +#include <crepe/api/Script.h> + +#include "ZapperObject.h" +#include "util/OptionalRef.h" + +class ZapperPoolSubScene; + +class ZapperPoolScript : public crepe::Script { +public: +	ZapperPoolScript(std::vector<ZapperObject> && pool); + +	void init(); +	void fixed_update(crepe::duration_t); + +	unsigned i = 0; + +private: +	std::vector<ZapperObject> pool; + +private: +	crepe::OptionalRef<crepe::Transform> camera_transform; +	crepe::OptionalRef<crepe::Camera> camera_camera; +	crepe::OptionalRef<ZapperObject> get_next_zapper(); + +private: +	void spawn_random(); + +private: +	static constexpr float OFFSCREEN_MARGIN = 40; +}; + diff --git a/game/prefab/ZapperPoolSubScene.cpp b/game/prefab/ZapperPoolSubScene.cpp new file mode 100644 index 0000000..e341090 --- /dev/null +++ b/game/prefab/ZapperPoolSubScene.cpp @@ -0,0 +1,19 @@ +#include <crepe/api/BehaviorScript.h> + +#include "ZapperPoolSubScene.h" +#include "ZapperPoolScript.h" + +using namespace crepe; +using namespace std; + +ZapperPoolSubScene::ZapperPoolSubScene(Scene & scene) +	: controller { scene.new_object("controller") } { + +	Log::logf(Log::DEBUG, "Building zapper pool..."); +	vector<ZapperObject> pool; +	for (size_t i = 0; i < this->POOL_SIZE; i++) +		pool.emplace_back(scene.new_object("zapper", "zapper")); +	BehaviorScript & behavior = this->controller.add_component<BehaviorScript>(); +	behavior.set_script<ZapperPoolScript>(std::move(pool)); +} + diff --git a/game/prefab/ZapperPoolSubScene.h b/game/prefab/ZapperPoolSubScene.h new file mode 100644 index 0000000..f930e22 --- /dev/null +++ b/game/prefab/ZapperPoolSubScene.h @@ -0,0 +1,19 @@ +#pragma once + +#include <crepe/api/Scene.h> +#include <crepe/api/GameObject.h> +#include <crepe/api/Event.h> +#include <crepe/util/OptionalRef.h> + +class CreateZapperEvent : public crepe::Event {}; + +class ZapperPoolSubScene { +public: +	ZapperPoolSubScene(crepe::Scene & scene); + +private: +	crepe::GameObject controller; + +private: +	static constexpr size_t POOL_SIZE = 4; +}; diff --git a/game/util/scrollgen b/game/util/scrollgen new file mode 100755 index 0000000..0389107 --- /dev/null +++ b/game/util/scrollgen @@ -0,0 +1,36 @@ +#!/bin/sh +INPUT="$1" +FRAMES="$2" + +die() { +	echo "$@" +	exit 1 +} +check_command() { +	cmd="$1" +	command -v "$cmd" > /dev/null || die "command '$cmd' not found" +} + +check_command magick +check_command identify +[ "$#" -eq 2 ] || die "usage: $0 <input image> <frame count>" +[ -e "$INPUT" ] || die "file not found: $INPUT" +[ "$FRAMES" -gt 0 ] || die "invalid frame count: $FRAMES" + +tile_width=$(identify -format "%w" "$INPUT") +tile_height=$(identify -format "%h" "$INPUT") + +OUTPUT="$INPUT.scroll.png" +magick -size "${tile_width}x$(( $tile_height * $FRAMES ))" 'xc:#ff00ff00' "$OUTPUT" + +for i in $(seq 0 $(( $FRAMES - 1 ))); do +	offset_x=$(( $tile_width * $i / $FRAMES )) +	offset_y=$(( i * $tile_height )) + +	magick "$OUTPUT" "$INPUT" -geometry "+${offset_x}+${offset_y}" -composite "$OUTPUT" +	magick "$OUTPUT" "$INPUT" -geometry "+$(( $offset_x - $tile_width ))+${offset_y}" -composite "$OUTPUT" +	echo "+${offset_x}+${offset_y}" +done + +# magick -size ${total_width}x${sprite_height} xc:none canvas.png + |