diff options
31 files changed, 747 insertions, 295 deletions
| diff --git a/src/crepe/CMakeLists.txt b/src/crepe/CMakeLists.txt index da9d492..6cbb9fe 100644 --- a/src/crepe/CMakeLists.txt +++ b/src/crepe/CMakeLists.txt @@ -2,6 +2,7 @@ target_sources(crepe PUBLIC  	Particle.cpp  	Component.cpp  	Collider.cpp +	Resource.cpp  )  target_sources(crepe PUBLIC FILE_SET HEADERS FILES @@ -9,6 +10,7 @@ target_sources(crepe PUBLIC FILE_SET HEADERS FILES  	Collider.h  	ValueBroker.h  	ValueBroker.hpp +	Resource.h  )  add_subdirectory(api) diff --git a/src/crepe/Component.h b/src/crepe/Component.h index c30419d..060ff00 100644 --- a/src/crepe/Component.h +++ b/src/crepe/Component.h @@ -16,7 +16,12 @@ class Component {  public:  	//! Whether the component is active  	bool active = true; -	//! The id of the GameObject this component belongs to +	/** +	 * \brief The id of the GameObject this component belongs to +	 * +	 * \note Only systems are supposed to use this member, but since friend +	 * relations aren't inherited this needs to be public. +	 */  	const game_object_id_t game_object_id;  protected: @@ -24,7 +29,7 @@ protected:  	 * \param id The id of the GameObject this component belongs to  	 */  	Component(game_object_id_t id); -	//! Only the ComponentManager can create components +	//! Only ComponentManager can create components  	friend class ComponentManager;  	Component(const Component &) = delete; @@ -45,6 +50,8 @@ public:  	 * \return The maximum number of instances for this component  	 */  	virtual int get_instances_max() const { return -1; } +	//! Only ComponentManager needs to know the max instance count +	friend class ComponentManager;  };  } // namespace crepe diff --git a/src/crepe/Resource.cpp b/src/crepe/Resource.cpp new file mode 100644 index 0000000..27b4c4b --- /dev/null +++ b/src/crepe/Resource.cpp @@ -0,0 +1,5 @@ +#include "Resource.h" + +using namespace crepe; + +Resource::Resource(const Asset & asset) {} diff --git a/src/crepe/Resource.h b/src/crepe/Resource.h new file mode 100644 index 0000000..eceb15b --- /dev/null +++ b/src/crepe/Resource.h @@ -0,0 +1,29 @@ +#pragma once + +namespace crepe { + +class ResourceManager; +class Asset; + +/** + * \brief Resource interface + * + * Resource is an interface class used to represent a (deserialized) game resource (e.g. + * textures, sounds). Resources are always created from \ref Asset "assets" by ResourceManager. + * + * The game programmer has the ability to use the ResourceManager to keep instances of concrete + * resources between scenes, preventing them from being reinstantiated during a scene + * transition. + */ +class Resource { +public: +	Resource(const Asset & src); +	virtual ~Resource() = default; + +	Resource(const Resource &) = delete; +	Resource(Resource &&) = delete; +	Resource & operator=(const Resource &) = delete; +	Resource & operator=(Resource &&) = delete; +}; + +} // namespace crepe diff --git a/src/crepe/api/AssetManager.cpp b/src/crepe/api/AssetManager.cpp deleted file mode 100644 index 3925758..0000000 --- a/src/crepe/api/AssetManager.cpp +++ /dev/null @@ -1,17 +0,0 @@ -#include "util/Log.h" - -#include "AssetManager.h" - -using namespace crepe; - -AssetManager & AssetManager::get_instance() { -	static AssetManager instance; -	return instance; -} - -AssetManager::~AssetManager() { -	dbg_trace(); -	this->asset_cache.clear(); -} - -AssetManager::AssetManager() { dbg_trace(); } diff --git a/src/crepe/api/AssetManager.h b/src/crepe/api/AssetManager.h deleted file mode 100644 index 3b1cc4b..0000000 --- a/src/crepe/api/AssetManager.h +++ /dev/null @@ -1,62 +0,0 @@ -#pragma once - -#include <any> -#include <memory> -#include <string> -#include <unordered_map> - -namespace crepe { - -/** - * \brief The AssetManager is responsible for storing and managing assets over multiple scenes. - * - * The AssetManager ensures that assets are loaded once and can be accessed across different - * scenes. It caches assets to avoid reloading them every time a scene is loaded. Assets are - * retained in memory until the AssetManager is destroyed, at which point the cached assets are - * cleared. - */ -class AssetManager { - -private: -	//! A cache that holds all the assets, accessible by their file path, over multiple scenes. -	std::unordered_map<std::string, std::any> asset_cache; - -private: -	AssetManager(); -	virtual ~AssetManager(); - -public: -	AssetManager(const AssetManager &) = delete; -	AssetManager(AssetManager &&) = delete; -	AssetManager & operator=(const AssetManager &) = delete; -	AssetManager & operator=(AssetManager &&) = delete; - -	/** -	 * \brief Retrieves the singleton instance of the AssetManager. -	 * -	 * \return A reference to the single instance of the AssetManager. -	 */ -	static AssetManager & get_instance(); - -public: -	/** -	 * \brief Caches an asset by loading it from the given file path. -	 * -	 * \param file_path The path to the asset file to load. -	 * \param reload If true, the asset will be reloaded from the file, even if it is already -	 * cached. -	 * \tparam T The type of asset to cache (e.g., texture, sound, etc.). -	 * -	 * \return A shared pointer to the cached asset. -	 * -	 * This template function caches the asset at the given file path. If the asset is already -	 * cached and `reload` is false, the existing cached version will be returned. Otherwise, the -	 * asset will be reloaded and added to the cache. -	 */ -	template <typename T> -	std::shared_ptr<T> cache(const std::string & file_path, bool reload = false); -}; - -} // namespace crepe - -#include "AssetManager.hpp" diff --git a/src/crepe/api/AssetManager.hpp b/src/crepe/api/AssetManager.hpp deleted file mode 100644 index 1c0e978..0000000 --- a/src/crepe/api/AssetManager.hpp +++ /dev/null @@ -1,22 +0,0 @@ -#pragma once - -#include "AssetManager.h" - -namespace crepe { - -template <typename asset> -std::shared_ptr<asset> AssetManager::cache(const std::string & file_path, bool reload) { -	auto it = asset_cache.find(file_path); - -	if (!reload && it != asset_cache.end()) { -		return std::any_cast<std::shared_ptr<asset>>(it->second); -	} - -	std::shared_ptr<asset> new_asset = std::make_shared<asset>(file_path.c_str()); - -	asset_cache[file_path] = new_asset; - -	return new_asset; -} - -} // namespace crepe diff --git a/src/crepe/api/AudioSource.cpp b/src/crepe/api/AudioSource.cpp new file mode 100644 index 0000000..7b05cb1 --- /dev/null +++ b/src/crepe/api/AudioSource.cpp @@ -0,0 +1,15 @@ +#include "AudioSource.h" + +using namespace crepe; +using namespace std; + +AudioSource::AudioSource(game_object_id_t id, const Asset & src) +	: Component(id), +	  source(src) {} + +void AudioSource::play(bool looping) { +	this->loop = looping; +	this->oneshot_play = true; +} + +void AudioSource::stop() { this->oneshot_stop = true; } diff --git a/src/crepe/api/AudioSource.h b/src/crepe/api/AudioSource.h new file mode 100644 index 0000000..b20e490 --- /dev/null +++ b/src/crepe/api/AudioSource.h @@ -0,0 +1,74 @@ +#pragma once + +#include "../Component.h" +#include "../facade/SoundHandle.h" +#include "../types.h" + +#include "Asset.h" +#include "GameObject.h" + +namespace crepe { + +class AudioSystem; + +//! Audio source component +class AudioSource : public Component { +	//! AudioSource components are handled by AudioSystem +	friend class AudioSystem; + +protected: +	/** +	 * \param source Sound sample to load +	 */ +	AudioSource(game_object_id_t id, const Asset & source); +	//! Only ComponentManager creates components +	friend class ComponentManager; + +public: +	// std::unique_ptr needs to be able to destoy this component +	virtual ~AudioSource() = default; + +public: +	//! Start this audio source +	void play(bool looping = false); +	//! Stop this audio source +	void stop(); + +public: +	//! Play when this component becomes active +	bool play_on_awake = false; +	//! Repeat the current audio clip during playback +	bool loop = false; +	//! Normalized volume (0.0 - 1.0) +	float volume = 1.0; + +private: +	//! This audio source's clip +	const Asset source; + +	/** +	 * \name One-shot state variables +	 * +	 * These variables trigger function calls when set to true, and are unconditionally reset on +	 * every system update. +	 * +	 * \{ +	 */ +	//! Play this sample +	bool oneshot_play = false; +	//! Stop this sample +	bool oneshot_stop = false; +	//! \} +	/** +	 * \name State diffing variables +	 * \{ +	 */ +	typeof(active) last_active = false; +	typeof(volume) last_volume = volume; +	typeof(loop) last_loop = loop; +	//! \} +	//! This source's voice handle +	SoundHandle voice{}; +}; + +} // namespace crepe diff --git a/src/crepe/api/CMakeLists.txt b/src/crepe/api/CMakeLists.txt index 593c4e6..a163faf 100644 --- a/src/crepe/api/CMakeLists.txt +++ b/src/crepe/api/CMakeLists.txt @@ -1,5 +1,5 @@  target_sources(crepe PUBLIC -	# AudioSource.cpp +	AudioSource.cpp  	BehaviorScript.cpp  	GameObject.cpp  	Rigidbody.cpp @@ -7,7 +7,6 @@ target_sources(crepe PUBLIC  	Transform.cpp  	Color.cpp  	Texture.cpp -	AssetManager.cpp  	Sprite.cpp  	Config.cpp  	Metadata.cpp @@ -27,7 +26,7 @@ target_sources(crepe PUBLIC  )  target_sources(crepe PUBLIC FILE_SET HEADERS FILES -	# AudioSource.h +	AudioSource.h  	BehaviorScript.h  	Config.h  	Script.h @@ -40,8 +39,6 @@ target_sources(crepe PUBLIC FILE_SET HEADERS FILES  	Vector2.hpp  	Color.h  	Texture.h -	AssetManager.h -	AssetManager.hpp  	Scene.h  	Metadata.h  	Camera.h diff --git a/src/crepe/api/Config.h b/src/crepe/api/Config.h index a9745c3..6472270 100644 --- a/src/crepe/api/Config.h +++ b/src/crepe/api/Config.h @@ -15,20 +15,10 @@ namespace crepe {   * modified *before* execution is handed over from the game programmer to the engine (i.e. the   * main loop is started).   */ -class Config final { -public: +struct Config final {  	//! Retrieve handle to global Config instance  	static Config & get_instance(); -private: -	Config() = default; -	~Config() = default; -	Config(const Config &) = default; -	Config(Config &&) = default; -	Config & operator=(const Config &) = default; -	Config & operator=(Config &&) = default; - -public:  	//! Logging-related settings  	struct {  		/** @@ -86,6 +76,12 @@ public:  		 */  		std::string root_pattern = ".crepe-root";  	} asset; + +	//! Audio system settings +	struct { +		//! Max amount of simultanious voices +		unsigned int voices = 32; +	} audio;  };  } // namespace crepe diff --git a/src/crepe/facade/Sound.cpp b/src/crepe/facade/Sound.cpp index 4d3abf5..ad50637 100644 --- a/src/crepe/facade/Sound.cpp +++ b/src/crepe/facade/Sound.cpp @@ -1,59 +1,13 @@ +#include "../api/Asset.h"  #include "../util/Log.h"  #include "Sound.h" -#include "SoundContext.h"  using namespace crepe;  using namespace std; -Sound::Sound(unique_ptr<Asset> res) { +Sound::Sound(const Asset & src) : Resource(src) { +	this->sample.load(src.get_path().c_str());  	dbg_trace(); -	this->load(std::move(res)); -} - -Sound::Sound(const char * src) { -	dbg_trace(); -	this->load(make_unique<Asset>(src)); -} - -void Sound::load(unique_ptr<Asset> res) { this->sample.load(res->get_path().c_str()); } - -void Sound::play() { -	SoundContext & ctx = SoundContext::get_instance(); -	if (ctx.engine.getPause(this->handle)) { -		// resume if paused -		ctx.engine.setPause(this->handle, false); -	} else { -		// or start new sound -		this->handle = ctx.engine.play(this->sample, this->volume); -		ctx.engine.setLooping(this->handle, this->looping); -	} -} - -void Sound::pause() { -	SoundContext & ctx = SoundContext::get_instance(); -	if (ctx.engine.getPause(this->handle)) return; -	ctx.engine.setPause(this->handle, true); -} - -void Sound::rewind() { -	SoundContext & ctx = SoundContext::get_instance(); -	if (!ctx.engine.isValidVoiceHandle(this->handle)) return; -	ctx.engine.seek(this->handle, 0); -} - -void Sound::set_volume(float volume) { -	this->volume = volume; - -	SoundContext & ctx = SoundContext::get_instance(); -	if (!ctx.engine.isValidVoiceHandle(this->handle)) return; -	ctx.engine.setVolume(this->handle, this->volume); -} - -void Sound::set_looping(bool looping) { -	this->looping = looping; - -	SoundContext & ctx = SoundContext::get_instance(); -	if (!ctx.engine.isValidVoiceHandle(this->handle)) return; -	ctx.engine.setLooping(this->handle, this->looping);  } +Sound::~Sound() { dbg_trace(); } diff --git a/src/crepe/facade/Sound.h b/src/crepe/facade/Sound.h index ee43d94..85d141b 100644 --- a/src/crepe/facade/Sound.h +++ b/src/crepe/facade/Sound.h @@ -1,84 +1,30 @@  #pragma once -#include <memory>  #include <soloud/soloud.h>  #include <soloud/soloud_wav.h> -#include "../api/Asset.h" +#include "../Resource.h"  namespace crepe { +class SoundContext; +  /**   * \brief Sound resource facade   * - * This class is a wrapper around a \c SoLoud::Wav instance, which holds a - * single sample. It is part of the sound facade. + * This class is a wrapper around a \c SoLoud::Wav instance, which holds a single sample. It is + * part of the sound facade.   */ -class Sound { -public: -	/** -	 * \brief Pause this sample -	 * -	 * Pauses this sound if it is playing, or does nothing if it is already paused. The playhead -	 * position is saved, such that calling \c play() after this function makes the sound resume. -	 */ -	void pause(); -	/** -	 * \brief Play this sample -	 * -	 * Resume playback if this sound is paused, or start from the beginning of the sample. -	 * -	 * \note This class only saves a reference to the most recent 'voice' of this sound. Calling -	 * \c play() while the sound is already playing causes multiple instances of the sample to -	 * play simultaniously. The sample started last is the one that is controlled afterwards. -	 */ -	void play(); -	/** -	 * \brief Reset playhead position -	 * -	 * Resets the playhead position so that calling \c play() after this function makes it play -	 * from the start of the sample. If the sound is not paused before calling this function, -	 * this function will stop playback. -	 */ -	void rewind(); -	/** -	 * \brief Set playback volume / gain -	 * -	 * \param volume  Volume (0 = muted, 1 = full volume) -	 */ -	void set_volume(float volume); -	/** -	 * \brief Get playback volume / gain -	 * -	 * \return Volume -	 */ -	float get_volume() const { return this->volume; } -	/** -	 * \brief Set looping behavior for this sample -	 * -	 * \param looping  Looping behavior (false = one-shot, true = loop) -	 */ -	void set_looping(bool looping); -	/** -	 * \brief Get looping behavior -	 * -	 * \return true if looping, false if one-shot -	 */ -	bool get_looping() const { return this->looping; } - +class Sound : public Resource {  public: -	Sound(const char * src); -	Sound(std::unique_ptr<Asset> res); - -private: -	void load(std::unique_ptr<Asset> res); +	Sound(const Asset & src); +	~Sound(); // dbg_trace  private: +	//! Deserialized resource (soloud)  	SoLoud::Wav sample; -	SoLoud::handle handle; - -	float volume = 1.0f; -	bool looping = false; +	//! SoundContext uses \c sample +	friend class SoundContext;  };  } // namespace crepe diff --git a/src/crepe/facade/SoundContext.cpp b/src/crepe/facade/SoundContext.cpp index deb2b62..b1f8cb3 100644 --- a/src/crepe/facade/SoundContext.cpp +++ b/src/crepe/facade/SoundContext.cpp @@ -4,17 +4,33 @@  using namespace crepe; -SoundContext & SoundContext::get_instance() { -	static SoundContext instance; -	return instance; -} -  SoundContext::SoundContext() {  	dbg_trace(); -	engine.init(); +	this->engine.init(); +	this->engine.setMaxActiveVoiceCount(this->config.audio.voices);  }  SoundContext::~SoundContext() {  	dbg_trace(); -	engine.deinit(); +	this->engine.deinit(); +} + +SoundHandle SoundContext::play(Sound & resource) { +	SoLoud::handle real_handle = this->engine.play(resource.sample, 1.0f); +	SoundHandle handle = this->next_handle; +	this->registry[handle] = real_handle; +	this->next_handle++; +	return handle; +} + +void SoundContext::stop(const SoundHandle & handle) { +	this->engine.stop(this->registry[handle]); +} + +void SoundContext::set_volume(const SoundHandle & handle, float volume) { +	this->engine.setVolume(this->registry[handle], volume); +} + +void SoundContext::set_loop(const SoundHandle & handle, bool loop) { +	this->engine.setLooping(this->registry[handle], loop);  } diff --git a/src/crepe/facade/SoundContext.h b/src/crepe/facade/SoundContext.h index d703c16..d986c59 100644 --- a/src/crepe/facade/SoundContext.h +++ b/src/crepe/facade/SoundContext.h @@ -2,30 +2,80 @@  #include <soloud/soloud.h> +#include "../api/Config.h" +  #include "Sound.h" +#include "SoundHandle.h"  namespace crepe {  /**   * \brief Sound engine facade   * - * This class is a wrapper around a \c SoLoud::Soloud instance, which provides - * the methods for playing \c Sound instances. It is part of the sound facade. + * This class is a wrapper around a \c SoLoud::Soloud instance, which provides the methods for + * playing \c Sound instances. It is part of the sound facade.   */  class SoundContext { -private: -	// singleton +public:  	SoundContext();  	virtual ~SoundContext(); +  	SoundContext(const SoundContext &) = delete;  	SoundContext(SoundContext &&) = delete;  	SoundContext & operator=(const SoundContext &) = delete;  	SoundContext & operator=(SoundContext &&) = delete; +	/** +	 * \brief Play a sample +	 * +	 * Plays a Sound from the beginning of the sample and returns a handle to control it later. +	 * +	 * \param resource Sound instance to play +	 * +	 * \returns Handle to control this voice +	 */ +	virtual SoundHandle play(Sound & resource); +	/** +	 * \brief Stop a voice immediately if it is still playing +	 * +	 * \note This function does nothing if the handle is invalid or if the sound is already +	 * stopped / finished playing. +	 * +	 * \param handle Voice handle returned by SoundContext::play +	 */ +	virtual void stop(const SoundHandle & handle); +	/** +	 * \brief Change the volume of a voice +	 * +	 * \note This function does nothing if the handle is invalid or if the sound is already +	 * stopped / finished playing. +	 * +	 * \param handle Voice handle returned by SoundContext::play +	 * \param volume New gain value (0=silent, 1=default) +	 */ +	virtual void set_volume(const SoundHandle & handle, float volume); +	/** +	 * \brief Set the looping behavior of a voice +	 * +	 * \note This function does nothing if the handle is invalid or if the sound is already +	 * stopped / finished playing. +	 * +	 * \param handle Voice handle returned by SoundContext::play +	 * \param loop Looping behavior (false=oneshot, true=loop) +	 */ +	virtual void set_loop(const SoundHandle & handle, bool loop); +  private: -	static SoundContext & get_instance(); +	//! Abstracted class  	SoLoud::Soloud engine; -	friend class Sound; + +	//! Config reference +	Config & config = Config::get_instance(); + +	//! Sound handle registry +	std::unordered_map<SoundHandle, SoLoud::handle> registry; +	//! Unique handle counter +	SoundHandle next_handle = 0;  };  } // namespace crepe diff --git a/src/crepe/facade/SoundHandle.h b/src/crepe/facade/SoundHandle.h new file mode 100644 index 0000000..b7925fc --- /dev/null +++ b/src/crepe/facade/SoundHandle.h @@ -0,0 +1,12 @@ +#pragma once + +#include <cstddef> + +namespace crepe { + +/** + * \brief Voice handle returned by + */ +typedef size_t SoundHandle; + +} // namespace crepe diff --git a/src/crepe/manager/CMakeLists.txt b/src/crepe/manager/CMakeLists.txt index 517b8a2..480c8ee 100644 --- a/src/crepe/manager/CMakeLists.txt +++ b/src/crepe/manager/CMakeLists.txt @@ -4,6 +4,7 @@ target_sources(crepe PUBLIC  	Manager.cpp  	SaveManager.cpp  	SceneManager.cpp +	ResourceManager.cpp  )  target_sources(crepe PUBLIC FILE_SET HEADERS FILES @@ -16,5 +17,7 @@ target_sources(crepe PUBLIC FILE_SET HEADERS FILES  	SaveManager.h  	SceneManager.h  	SceneManager.hpp +	ResourceManager.h +	ResourceManager.hpp  ) diff --git a/src/crepe/manager/Mediator.h b/src/crepe/manager/Mediator.h index 8094d80..ba0b41f 100644 --- a/src/crepe/manager/Mediator.h +++ b/src/crepe/manager/Mediator.h @@ -12,6 +12,7 @@ namespace crepe {  class ComponentManager;  class SceneManager; +class ResourceManager;  /**   * Struct to pass references to classes that would otherwise need to be singletons down to @@ -30,6 +31,7 @@ struct Mediator {  	OptionalRef<SceneManager> scene_manager;  	OptionalRef<SaveManager> save_manager = SaveManager::get_instance();  	OptionalRef<EventManager> event_manager = EventManager::get_instance(); +	OptionalRef<ResourceManager> resource_manager;  	OptionalRef<SDLContext> sdl_context = SDLContext::get_instance();  	OptionalRef<LoopTimer> timer = LoopTimer::get_instance();  }; diff --git a/src/crepe/manager/ResourceManager.cpp b/src/crepe/manager/ResourceManager.cpp new file mode 100644 index 0000000..7c01808 --- /dev/null +++ b/src/crepe/manager/ResourceManager.cpp @@ -0,0 +1,30 @@ +#include "util/Log.h" + +#include "ResourceManager.h" + +using namespace crepe; +using namespace std; + +ResourceManager::ResourceManager(Mediator & mediator) : Manager(mediator) { +	mediator.resource_manager = *this; +	dbg_trace(); +} +ResourceManager::~ResourceManager() { dbg_trace(); } + +void ResourceManager::clear() { +	std::erase_if(this->resources, [](const pair<const Asset, CacheEntry> & pair) { +		const CacheEntry & entry = pair.second; +		return entry.persistent == false; +	}); +} + +void ResourceManager::clear_all() { this->resources.clear(); } + +void ResourceManager::set_persistent(const Asset & asset, bool persistent) { +	this->get_entry(asset).persistent = persistent; +} + +ResourceManager::CacheEntry & ResourceManager::get_entry(const Asset & asset) { +	if (!this->resources.contains(asset)) this->resources[asset] = {}; +	return this->resources.at(asset); +} diff --git a/src/crepe/manager/ResourceManager.h b/src/crepe/manager/ResourceManager.h new file mode 100644 index 0000000..84b275d --- /dev/null +++ b/src/crepe/manager/ResourceManager.h @@ -0,0 +1,78 @@ +#pragma once + +#include <memory> +#include <unordered_map> + +#include "../Resource.h" +#include "../api/Asset.h" + +#include "Manager.h" + +namespace crepe { + +/** + * \brief Owner of concrete Resource instances + *  + * ResourceManager caches concrete Resource instances per Asset. Concrete resources are + * destroyed at the end of scenes by default, unless the game programmer marks them as + * persistent. + */ +class ResourceManager : public Manager { +public: +	ResourceManager(Mediator & mediator); +	virtual ~ResourceManager(); // dbg_trace + +private: +	//! Cache entry +	struct CacheEntry { +		//! Concrete resource instance +		std::unique_ptr<Resource> resource = nullptr; +		//! Prevent ResourceManager::clear from removing this entry +		bool persistent = false; +	}; +	//! Internal cache +	std::unordered_map<const Asset, CacheEntry> resources; +	/** +	 * \brief Ensure a cache entry exists for this asset and return a mutable reference to it +	 * +	 * \param asset Asset the concrete resource is instantiated from +	 * +	 * \returns Mutable reference to cache entry +	 */ +	CacheEntry & get_entry(const Asset & asset); + +public: +	/** +	 * \brief Mark a resource as persistent (i.e. used across multiple scenes) +	 * +	 * \param asset Asset the concrete resource is instantiated from +	 * \param persistent Whether this resource is persistent (true=keep, false=destroy) +	 */ +	void set_persistent(const Asset & asset, bool persistent); + +	/** +	 * \brief Retrieve reference to concrete Resource by Asset +	 * +	 * \param asset Asset the concrete resource is instantiated from +	 * \tparam Resource Concrete derivative of Resource +	 * +	 * This class instantiates the concrete resource if it is not yet stored in the internal +	 * cache, or returns a reference to the cached resource if it already exists. +	 * +	 * \returns Reference to concrete resource +	 * +	 * \throws std::runtime_error if the \c Resource parameter does not match with the actual +	 * type of the resource stored in the cache for this Asset +	 */ +	template <typename Resource> +	Resource & get(const Asset & asset); + +	//! Clear non-persistent resources from cache +	void clear(); +	//! Clear all resources from cache regardless of persistence +	void clear_all(); +}; + +} // namespace crepe + +#include "ResourceManager.hpp" diff --git a/src/crepe/manager/ResourceManager.hpp b/src/crepe/manager/ResourceManager.hpp new file mode 100644 index 0000000..5167d71 --- /dev/null +++ b/src/crepe/manager/ResourceManager.hpp @@ -0,0 +1,27 @@ +#pragma once + +#include <format> + +#include "ResourceManager.h" + +namespace crepe { + +template <typename T> +T & ResourceManager::get(const Asset & asset) { +	using namespace std; +	static_assert(is_base_of<Resource, T>::value, +				  "cache must recieve a derivative class of Resource"); + +	CacheEntry & entry = this->get_entry(asset); +	if (entry.resource == nullptr) entry.resource = make_unique<T>(asset); + +	T * concrete_resource = dynamic_cast<T *>(entry.resource.get()); +	if (concrete_resource == nullptr) +		throw runtime_error(format("ResourceManager: mismatch between requested type and " +								   "actual type of resource ({})", +								   asset.get_path())); + +	return *concrete_resource; +} + +} // namespace crepe diff --git a/src/crepe/system/AudioSystem.cpp b/src/crepe/system/AudioSystem.cpp new file mode 100644 index 0000000..b2c1dc6 --- /dev/null +++ b/src/crepe/system/AudioSystem.cpp @@ -0,0 +1,63 @@ +#include "AudioSystem.h" + +#include "../manager/ComponentManager.h" +#include "../manager/ResourceManager.h" +#include "../types.h" + +using namespace crepe; +using namespace std; + +void AudioSystem::update() { +	ComponentManager & component_manager = this->mediator.component_manager; +	ResourceManager & resource_manager = this->mediator.resource_manager; +	RefVector<AudioSource> components +		= component_manager.get_components_by_type<AudioSource>(); + +	for (AudioSource & component : components) { +		Sound & resource = resource_manager.get<Sound>(component.source); + +		this->diff_update(component, resource); + +		this->update_last(component); +	} +} + +void AudioSystem::diff_update(AudioSource & component, Sound & resource) { +	SoundContext & context = this->get_context(); + +	if (component.active != component.last_active) { +		if (component.active) { +			component.oneshot_play = component.play_on_awake; +		} else { +			context.stop(component.voice); +			return; +		} +	} +	if (!component.active) return; + +	if (component.oneshot_play) { +		component.voice = context.play(resource); +		component.oneshot_play = false; +	} +	if (component.oneshot_stop) { +		context.stop(component.voice); +		component.oneshot_stop = false; +	} +	if (component.volume != component.last_volume) { +		context.set_volume(component.voice, component.volume); +	} +	if (component.loop != component.last_loop) { +		context.set_loop(component.voice, component.loop); +	} +} + +void AudioSystem::update_last(AudioSource & component) { +	component.last_active = component.active; +	component.last_loop = component.loop; +	component.last_volume = component.volume; +} + +SoundContext & AudioSystem::get_context() { +	if (this->context == nullptr) this->context = make_unique<SoundContext>(); +	return *this->context.get(); +} diff --git a/src/crepe/system/AudioSystem.h b/src/crepe/system/AudioSystem.h new file mode 100644 index 0000000..2ddc443 --- /dev/null +++ b/src/crepe/system/AudioSystem.h @@ -0,0 +1,51 @@ +#pragma once + +#include "../api/AudioSource.h" +#include "../facade/Sound.h" +#include "../facade/SoundContext.h" + +#include "System.h" + +namespace crepe { + +class AudioSystem : public System { +public: +	using System::System; +	void update() override; + +private: +	/** +	 * \brief Update `last_*` members of \c component +	 * +	 * Copies all component properties stored for comparison between AudioSystem::update() calls +	 * +	 * \param component AudioSource component to update +	 */ +	void update_last(AudioSource & component); + +	/** +	 * \brief Compare update component +	 * +	 * Compares properties of \c component and \c data, and calls SoundContext functions where +	 * applicable. +	 * +	 * \param component AudioSource component to update +	 * \param resource Sound instance for AudioSource's Asset +	 */ +	void diff_update(AudioSource & component, Sound & resource); + +protected: +	/** +	 * \brief Get SoundContext +	 * +	 * SoundContext is retrieved through this function instead of being a direct member of +	 * AudioSystem to aid with testability. +	 */ +	virtual SoundContext & get_context(); + +private: +	//! SoundContext +	std::unique_ptr<SoundContext> context = nullptr; +}; + +} // namespace crepe diff --git a/src/crepe/system/CMakeLists.txt b/src/crepe/system/CMakeLists.txt index 95f6e33..6b2e099 100644 --- a/src/crepe/system/CMakeLists.txt +++ b/src/crepe/system/CMakeLists.txt @@ -5,6 +5,7 @@ target_sources(crepe PUBLIC  	PhysicsSystem.cpp  	CollisionSystem.cpp  	RenderSystem.cpp +	AudioSystem.cpp  	AnimatorSystem.cpp  	InputSystem.cpp  ) @@ -15,6 +16,7 @@ target_sources(crepe PUBLIC FILE_SET HEADERS FILES  	PhysicsSystem.h  	CollisionSystem.h  	RenderSystem.h +	AudioSystem.h  	AnimatorSystem.h  	InputSystem.h  ) diff --git a/src/crepe/system/System.cpp b/src/crepe/system/System.cpp index f68549b..ecc740d 100644 --- a/src/crepe/system/System.cpp +++ b/src/crepe/system/System.cpp @@ -1,7 +1,5 @@ -#include "../util/Log.h" -  #include "System.h"  using namespace crepe; -System::System(const Mediator & mediator) : mediator(mediator) { dbg_trace(); } +System::System(const Mediator & mediator) : mediator(mediator) {} diff --git a/src/example/CMakeLists.txt b/src/example/CMakeLists.txt index 8ef71bb..73fc512 100644 --- a/src/example/CMakeLists.txt +++ b/src/example/CMakeLists.txt @@ -16,7 +16,6 @@ function(add_example target_name)  	add_dependencies(examples ${target_name})  endfunction() -add_example(asset_manager)  add_example(savemgr)  add_example(rendering_particle)  add_example(game) diff --git a/src/example/asset_manager.cpp b/src/example/asset_manager.cpp deleted file mode 100644 index 917b547..0000000 --- a/src/example/asset_manager.cpp +++ /dev/null @@ -1,36 +0,0 @@ -#include <crepe/api/AssetManager.h> -#include <crepe/api/Texture.h> -#include <crepe/facade/Sound.h> - -using namespace crepe; - -int main() { - -	// this needs to be called before the asset manager otherwise the destructor of sdl is not in -	// the right order -	{ Texture test("../asset/texture/img.png"); } -	// FIXME: make it so the issue described by the above comment is not possible (i.e. the order -	// in which internal classes are instantiated should not impact the way the engine works). - -	auto & mgr = AssetManager::get_instance(); - -	{ -		// TODO: [design] the Sound class can't be directly included by the user as it includes -		// SoLoud headers. -		auto bgm = mgr.cache<Sound>("../mwe/audio/bgm.ogg"); -		auto sfx1 = mgr.cache<Sound>("../mwe/audio/sfx1.wav"); -		auto sfx2 = mgr.cache<Sound>("../mwe/audio/sfx2.wav"); - -		auto img = mgr.cache<Texture>("../asset/texture/img.png"); -		auto img1 = mgr.cache<Texture>("../asset/texture/second.png"); -	} - -	{ -		auto bgm = mgr.cache<Sound>("../mwe/audio/bgm.ogg"); -		auto sfx1 = mgr.cache<Sound>("../mwe/audio/sfx1.wav"); -		auto sfx2 = mgr.cache<Sound>("../mwe/audio/sfx2.wav"); - -		auto img = mgr.cache<Texture>("../asset/texture/img.png"); -		auto img1 = mgr.cache<Texture>("../asset/texture/second.png"); -	} -} diff --git a/src/test/AudioTest.cpp b/src/test/AudioTest.cpp new file mode 100644 index 0000000..774fdb8 --- /dev/null +++ b/src/test/AudioTest.cpp @@ -0,0 +1,152 @@ +#include <gmock/gmock.h> +#include <gtest/gtest.h> + +#include <crepe/api/AudioSource.h> +#include <crepe/api/GameObject.h> +#include <crepe/manager/ComponentManager.h> +#include <crepe/manager/ResourceManager.h> +#include <crepe/system/AudioSystem.h> + +using namespace std; +using namespace std::chrono_literals; +using namespace crepe; +using namespace testing; + +class AudioTest : public Test { +private: +	class TestSoundContext : public SoundContext { +	public: +		MOCK_METHOD(SoundHandle, play, (Sound & resource), (override)); +		MOCK_METHOD(void, stop, (const SoundHandle &), (override)); +		MOCK_METHOD(void, set_volume, (const SoundHandle &, float), (override)); +		MOCK_METHOD(void, set_loop, (const SoundHandle &, bool), (override)); +	}; + +	class TestAudioSystem : public AudioSystem { +	public: +		using AudioSystem::AudioSystem; +		StrictMock<TestSoundContext> context; +		virtual SoundContext & get_context() { return this->context; } +	}; + +private: +	Mediator mediator; +	ComponentManager component_manager{mediator}; +	ResourceManager resource_manager{mediator}; + +public: +	TestAudioSystem system{mediator}; +	TestSoundContext & context = system.context; + +private: +	GameObject entity = component_manager.new_object("name"); + +public: +	AudioSource & component = entity.add_component<AudioSource>("mwe/audio/bgm.ogg"); +}; + +TEST_F(AudioTest, Default) { +	EXPECT_CALL(context, play(_)).Times(0); +	EXPECT_CALL(context, stop(_)).Times(0); +	EXPECT_CALL(context, set_volume(_, _)).Times(0); +	EXPECT_CALL(context, set_loop(_, _)).Times(0); +	system.update(); +} + +TEST_F(AudioTest, Play) { +	system.update(); + +	{ +		InSequence seq; + +		EXPECT_CALL(context, play(_)).Times(0); +		component.play(); +	} + +	{ +		InSequence seq; + +		EXPECT_CALL(context, play(_)).Times(1); +		system.update(); +	} +} + +TEST_F(AudioTest, Stop) { +	system.update(); + +	{ +		InSequence seq; + +		EXPECT_CALL(context, stop(_)).Times(0); +		component.stop(); +	} + +	{ +		InSequence seq; + +		EXPECT_CALL(context, stop(_)).Times(1); +		system.update(); +	} +} + +TEST_F(AudioTest, Volume) { +	system.update(); + +	{ +		InSequence seq; + +		EXPECT_CALL(context, set_volume(_, _)).Times(0); +		component.volume += 0.2; +	} + +	{ +		InSequence seq; + +		EXPECT_CALL(context, set_volume(_, component.volume)).Times(1); +		system.update(); +	} +} + +TEST_F(AudioTest, Looping) { +	system.update(); + +	{ +		InSequence seq; + +		EXPECT_CALL(context, set_loop(_, _)).Times(0); +		component.loop = !component.loop; +	} + +	{ +		InSequence seq; + +		EXPECT_CALL(context, set_loop(_, component.loop)).Times(1); +		system.update(); +	} +} + +TEST_F(AudioTest, StopOnDeactivate) { +	system.update(); + +	{ +		InSequence seq; + +		EXPECT_CALL(context, stop(_)).Times(1); +		component.active = false; +		system.update(); +	} +} + +TEST_F(AudioTest, PlayOnActive) { +	component.active = false; +	component.play_on_awake = true; +	system.update(); + +	{ +		InSequence seq; + +		EXPECT_CALL(context, play(_)).Times(1); +		component.active = true; +		system.update(); +	} +} diff --git a/src/test/CMakeLists.txt b/src/test/CMakeLists.txt index c9cbac5..9f986a0 100644 --- a/src/test/CMakeLists.txt +++ b/src/test/CMakeLists.txt @@ -4,7 +4,9 @@ target_sources(test_main PUBLIC  	PhysicsTest.cpp  	ScriptTest.cpp  	ParticleTest.cpp +	AudioTest.cpp  	AssetTest.cpp +	ResourceManagerTest.cpp  	OptionalRefTest.cpp  	RenderSystemTest.cpp  	EventTest.cpp diff --git a/src/test/ResourceManagerTest.cpp b/src/test/ResourceManagerTest.cpp new file mode 100644 index 0000000..44a5921 --- /dev/null +++ b/src/test/ResourceManagerTest.cpp @@ -0,0 +1,81 @@ +#include <gtest/gtest.h> + +#define private public +#define protected public + +#include <crepe/api/GameObject.h> +#include <crepe/manager/ResourceManager.h> +#include <crepe/util/Log.h> + +using namespace std; +using namespace crepe; +using namespace testing; + +class ResourceManagerTest : public Test { +	Mediator mediator; + +public: +	ResourceManager resource_manager{mediator}; + +	class Unrelated : public Resource { +		using Resource::Resource; +	}; + +	Asset asset_a{"asset/texture/img.png"}; +	Asset asset_b{"asset/texture/ERROR.png"}; + +	class TestResource : public Resource { +	public: +		static unsigned instances; + +	public: +		const unsigned instance; +		TestResource(const Asset & src) : Resource(src), instance(this->instances++) {} +		~TestResource() { this->instances--; } +		bool operator==(const TestResource & other) const { +			return this->instance == other.instance; +		} +	}; + +private: +	void SetUp() override { TestResource::instances = 0; } +}; +unsigned ResourceManagerTest::TestResource::instances = 0; + +TEST_F(ResourceManagerTest, Default) { +	TestResource & res_1 = resource_manager.get<TestResource>(asset_a); +	TestResource & res_2 = resource_manager.get<TestResource>(asset_a); +	TestResource & res_3 = resource_manager.get<TestResource>(asset_b); +	TestResource & res_4 = resource_manager.get<TestResource>(asset_b); + +	ASSERT_EQ(res_1, res_2); +	ASSERT_EQ(res_3, res_4); +	EXPECT_NE(res_1, res_3); + +	EXPECT_EQ(TestResource::instances, 2); + +	resource_manager.clear(); +} + +TEST_F(ResourceManagerTest, Persistent) { +	resource_manager.set_persistent(asset_a, true); +	EXPECT_EQ(TestResource::instances, 0); + +	resource_manager.get<TestResource>(asset_a); +	resource_manager.get<TestResource>(asset_a); +	resource_manager.get<TestResource>(asset_b); +	resource_manager.get<TestResource>(asset_b); +	EXPECT_EQ(TestResource::instances, 2); + +	resource_manager.clear(); +	EXPECT_EQ(TestResource::instances, 1); + +	resource_manager.clear_all(); +	EXPECT_EQ(TestResource::instances, 0); +} + +TEST_F(ResourceManagerTest, UnmatchedType) { +	EXPECT_NO_THROW({ resource_manager.get<TestResource>(asset_a); }); + +	EXPECT_THROW({ resource_manager.get<Unrelated>(asset_a); }, runtime_error); +} diff --git a/src/test/main.cpp b/src/test/main.cpp index aece72d..ed2aed5 100644 --- a/src/test/main.cpp +++ b/src/test/main.cpp @@ -1,9 +1,5 @@ -#include <gtest/gtest.h> - -#define protected public -#define private public -  #include <crepe/api/Config.h> +#include <gtest/gtest.h>  using namespace crepe;  using namespace testing; @@ -11,12 +7,14 @@ using namespace testing;  class GlobalConfigReset : public EmptyTestEventListener {  public:  	Config & cfg = Config::get_instance(); -	Config cfg_default = Config();  	// This function is called before each test  	void OnTestStart(const TestInfo &) override { -		cfg = cfg_default; -		cfg.log.level = Log::Level::WARNING; +		cfg = { +			.log = { +				.level = Log::Level::WARNING, +			}, +		};  	}  }; |