diff --git a/.codecov.yml b/.codecov.yml new file mode 100644 index 0000000..c1c3711 --- /dev/null +++ b/.codecov.yml @@ -0,0 +1,3 @@ +ignore: + - catch.hpp + - catch_main.hpp diff --git a/.travis.yml b/.travis.yml index ffa348e..18f7714 100644 --- a/.travis.yml +++ b/.travis.yml @@ -62,6 +62,8 @@ matrix: - os: osx osx_image: xcode10 compiler: clang + addons: { homebrew: { packages: ["lcov"] } } + after_success: ./scripts/upload_coverage.sh before_install: - eval "${MATRIX_EVAL}" script: diff --git a/CMakeLists.txt b/CMakeLists.txt index 4adf7e5..2f6875c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,6 +1,36 @@ cmake_minimum_required(VERSION 3.5 FATAL_ERROR) project(ecs) +# +# coverage mode +# + +option(ECS_BUILD_WITH_COVERAGE "Build with coverage" OFF) +if(ECS_BUILD_WITH_COVERAGE AND (CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang")) + add_definitions(-DECS_BUILD_WITH_COVERAGE) + set(ECS_COVERAGE_FLAGS "--coverage") + set(CMAKE_C_FLAGS_DEBUG "${CMAKE_C_FLAGS_DEBUG} ${ECS_COVERAGE_FLAGS}") + set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} ${ECS_COVERAGE_FLAGS}") + set(CMAKE_LINKER_FLAGS_DEBUG "${CMAKE_LINKER_FLAGS_DEBUG} ${ECS_COVERAGE_FLAGS}") +endif() + +# +# sanitizer mode +# + +option(ECS_BUILD_WITH_SANITIZER "Build with sanitizer" OFF) +if(ECS_BUILD_WITH_SANITIZER AND (CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang")) + add_definitions(-DECS_BUILD_WITH_SANITIZER) + set(ECS_SANITIZER_FLAGS "-fno-omit-frame-pointer -fsanitize=address") + set(CMAKE_C_FLAGS_DEBUG "${CMAKE_C_FLAGS_DEBUG} ${ECS_SANITIZER_FLAGS}") + set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} ${ECS_SANITIZER_FLAGS}") + set(CMAKE_LINKER_FLAGS_DEBUG "${CMAKE_LINKER_FLAGS_DEBUG} ${ECS_SANITIZER_FLAGS}") +endif() + +# +# tests executable +# + file(GLOB test_sources "*.cpp" "*.hpp") add_executable(${PROJECT_NAME} ${test_sources}) diff --git a/README.md b/README.md index eba976e..7867a5d 100644 --- a/README.md +++ b/README.md @@ -2,18 +2,21 @@ [![travis][badge.travis]][travis] [![appveyor][badge.appveyor]][appveyor] +[![codecov][badge.codecov]][codecov] [![language][badge.language]][language] [![license][badge.license]][license] [![paypal][badge.paypal]][paypal] -[badge.travis]: https://img.shields.io/travis/BlackMATov/ecs.hpp/master.svg?logo=travis&style=for-the-badge -[badge.appveyor]: https://img.shields.io/appveyor/ci/BlackMATov/ecs-hpp/master.svg?logo=appveyor&style=for-the-badge -[badge.language]: https://img.shields.io/badge/language-C%2B%2B14-red.svg?style=for-the-badge -[badge.license]: https://img.shields.io/badge/license-MIT-blue.svg?style=for-the-badge -[badge.paypal]: https://img.shields.io/badge/donate-PayPal-orange.svg?logo=paypal&colorA=00457C&style=for-the-badge +[badge.travis]: https://img.shields.io/travis/BlackMATov/ecs.hpp/master.svg?logo=travis +[badge.appveyor]: https://img.shields.io/appveyor/ci/BlackMATov/ecs-hpp/master.svg?logo=appveyor +[badge.codecov]: https://img.shields.io/codecov/c/github/BlackMATov/ecs-hpp/master.svg?logo=codecov +[badge.language]: https://img.shields.io/badge/language-C%2B%2B14-red.svg +[badge.license]: https://img.shields.io/badge/license-MIT-blue.svg +[badge.paypal]: https://img.shields.io/badge/donate-PayPal-orange.svg?logo=paypal&colorA=00457C [travis]: https://travis-ci.org/BlackMATov/ecs.hpp [appveyor]: https://ci.appveyor.com/project/BlackMATov/ecs-hpp +[codecov]: https://codecov.io/gh/BlackMATov/ecs.hpp [language]: https://en.wikipedia.org/wiki/C%2B%2B14 [license]: https://en.wikipedia.org/wiki/MIT_License [paypal]: https://www.paypal.me/matov diff --git a/ecs.hpp b/ecs.hpp index 1409a9e..60e5e16 100644 --- a/ecs.hpp +++ b/ecs.hpp @@ -6,6 +6,151 @@ #pragma once +#include +#include + +#include +#include +#include +#include + +// ----------------------------------------------------------------------------- +// +// config +// +// ----------------------------------------------------------------------------- + namespace ecs_hpp { + class world; + class entity; + + using entity_id = std::uint64_t; +} + +// ----------------------------------------------------------------------------- +// +// entity +// +// ----------------------------------------------------------------------------- + +namespace ecs_hpp +{ + class entity final { + public: + entity(world& owner); + entity(world& owner, entity_id id); + + const world& owner() const noexcept; + entity_id id() const noexcept; + + bool destroy(); + private: + world& owner_; + entity_id id_{0u}; + }; + + bool operator==(const entity& l, const entity& r) noexcept; + bool operator!=(const entity& l, const entity& r) noexcept; +} + +namespace std +{ + template <> + struct hash + : std::unary_function + { + std::size_t operator()(const ecs_hpp::entity& ent) const noexcept { + return std::hash()(ent.id()); + } + }; +} + +// ----------------------------------------------------------------------------- +// +// world +// +// ----------------------------------------------------------------------------- + +namespace ecs_hpp +{ + class world final { + public: + world(); + ~world() noexcept; + + entity create_entity(); + bool destroy_entity(const entity& ent); + bool is_entity_alive(const entity& ent) const noexcept; + private: + mutable std::mutex mutex_; + entity_id last_entity_id_{0u}; + std::unordered_set entities_; + }; +} + +// ----------------------------------------------------------------------------- +// +// entity impl +// +// ----------------------------------------------------------------------------- + +namespace ecs_hpp +{ + inline entity::entity(world& owner) + : owner_(owner) {} + + inline entity::entity(world& owner, entity_id id) + : owner_(owner) + , id_(id) {} + + inline const world& entity::owner() const noexcept { + return owner_; + } + + inline entity_id entity::id() const noexcept { + return id_; + } + + inline bool entity::destroy() { + return owner_.destroy_entity(*this); + } + + inline bool operator==(const entity& l, const entity& r) noexcept { + return l.id() == r.id(); + } + + inline bool operator!=(const entity& l, const entity& r) noexcept { + return !(l == r); + } +} + +// ----------------------------------------------------------------------------- +// +// world impl +// +// ----------------------------------------------------------------------------- + +namespace ecs_hpp +{ + inline world::world() = default; + inline world::~world() noexcept = default; + + inline entity world::create_entity() { + std::lock_guard guard(mutex_); + assert(last_entity_id_ < std::numeric_limits::max()); + auto ent = entity(*this, ++last_entity_id_); + entities_.insert(ent); + return ent; + } + + inline bool world::destroy_entity(const entity& ent) { + std::lock_guard guard(mutex_); + return entities_.erase(ent) > 0u; + } + + inline bool world::is_entity_alive(const entity& ent) const noexcept { + std::lock_guard guard(mutex_); + return entities_.count(ent) > 0u; + } } diff --git a/ecs_tests.cpp b/ecs_tests.cpp index 584701f..57bb26a 100644 --- a/ecs_tests.cpp +++ b/ecs_tests.cpp @@ -12,7 +12,60 @@ namespace ecs = ecs_hpp; namespace { + struct position { + int x{0}; + int y{0}; + + position() = default; + position(int nx, int ny) : x(nx), y(ny) {} + }; + + struct velocity { + int dx{0}; + int dy{0}; + + velocity() = default; + velocity(int ndx, int ndy) : dx(ndx), dy(ndy) {} + }; } -TEST_CASE("ecs") { +TEST_CASE("world") { + SECTION("entities") { + { + ecs::world w; + + ecs::entity e1{w}; + ecs::entity e2{w}; + + REQUIRE(e1 == e2); + REQUIRE_FALSE(w.is_entity_alive(e1)); + REQUIRE_FALSE(w.is_entity_alive(e2)); + + REQUIRE_FALSE(w.destroy_entity(e1)); + REQUIRE_FALSE(w.destroy_entity(e2)); + } + { + ecs::world w; + + auto e1 = w.create_entity(); + auto e2 = w.create_entity(); + + REQUIRE(e1 != e2); + REQUIRE(w.is_entity_alive(e1)); + REQUIRE(w.is_entity_alive(e2)); + + REQUIRE(w.destroy_entity(e1)); + REQUIRE_FALSE(w.is_entity_alive(e1)); + REQUIRE(w.is_entity_alive(e2)); + + REQUIRE(w.destroy_entity(e2)); + REQUIRE_FALSE(w.is_entity_alive(e1)); + REQUIRE_FALSE(w.is_entity_alive(e2)); + + REQUIRE_FALSE(w.destroy_entity(e1)); + REQUIRE_FALSE(w.destroy_entity(e2)); + } + } + SECTION("components") { + } } diff --git a/scripts/upload_coverage.sh b/scripts/upload_coverage.sh new file mode 100755 index 0000000..d13a427 --- /dev/null +++ b/scripts/upload_coverage.sh @@ -0,0 +1,17 @@ +#!/bin/bash +set -e + +BUILD_DIR=`dirname "$BASH_SOURCE"`/../build +mkdir -p $BUILD_DIR/coverage +cd $BUILD_DIR/coverage +cmake -DCMAKE_BUILD_TYPE=Debug -DECS_BUILD_WITH_COVERAGE=ON ../.. +cmake --build . -- -j8 + +lcov -d . -z +ctest --verbose + +lcov -d . -c -o "coverage.info" +lcov -r "coverage.info" "*/usr/*" "*/catch.hpp" "*/catch_main.cpp" -o "coverage.info" +lcov -l "coverage.info" + +bash <(curl -s https://codecov.io/bash) || echo "Codecov did not collect coverage reports"