diff --git a/examples/dyna/CMakeLists.txt b/examples/dyna/CMakeLists.txt new file mode 100644 index 00000000..a0284c41 --- /dev/null +++ b/examples/dyna/CMakeLists.txt @@ -0,0 +1,59 @@ +cmake_minimum_required(VERSION 3.14) +project(dyna LANGUAGES C CXX) + +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_EXPORT_COMPILE_COMMANDS ON) +set(CMAKE_COLOR_DIAGNOSTICS ON) + +include(cmake/CPM.cmake) + +CPMAddPackage( + NAME glad + VERSION 2.0.8 + GIT_REPOSITORY https://github.com/Dav1dde/glad.git + GIT_TAG glad2 +) + +add_subdirectory(${glad_SOURCE_DIR}/cmake ${CMAKE_CURRENT_BINARY_DIR}/glad) + +glad_add_library(glad_gl_core_33 STATIC API gl:core=3.3) + +find_package(SDL3 REQUIRED) +find_package(SDL3_image REQUIRED) + +add_executable(dyna + src/main.cpp + src/datapath.cpp + src/timer.cpp + src/gfx.cpp + src/texture.cpp + src/entity.cpp + src/world.cpp + src/map.cpp + src/player.cpp + src/monster.cpp + src/bomb.cpp + src/bonus.cpp + src/game.cpp +) + +target_link_libraries(dyna + PRIVATE + glad_gl_core_33 + SDL3::SDL3 + SDL3_image::SDL3_image +) + +target_include_directories(dyna PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src) + +# Mirror the data/ tree next to the executable so the game finds its assets +# when launched from the build directory (paths are resolved via SDL_GetBasePath). +add_custom_command(TARGET dyna POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_directory + ${CMAKE_CURRENT_SOURCE_DIR}/data + $/data + COMMENT "Copying data/ next to dyna executable" +) + +file(GENERATE OUTPUT .gitignore CONTENT "*") diff --git a/examples/dyna/LICENSE b/examples/dyna/LICENSE new file mode 100644 index 00000000..9ddb1b08 --- /dev/null +++ b/examples/dyna/LICENSE @@ -0,0 +1,7 @@ +Dyna.net copyright 2005 by Bartosz Taudul and Ralf WrzeĊ›niewski. + +This program (including source code and the asset it uses) is NOT licensed +for any use other than being an example of how to integrate Tracy Profiler. + +The license terms written in other parts of this repository DO NOT apply +here. diff --git a/examples/dyna/cmake/CPM.cmake b/examples/dyna/cmake/CPM.cmake new file mode 100644 index 00000000..a4d79344 --- /dev/null +++ b/examples/dyna/cmake/CPM.cmake @@ -0,0 +1,24 @@ +# SPDX-License-Identifier: MIT +# +# SPDX-FileCopyrightText: Copyright (c) 2019-2023 Lars Melchior and contributors + +set(CPM_DOWNLOAD_VERSION 0.42.3) +set(CPM_HASH_SUM "a609e875fd532b067174250f6abbc3dac22fe2d64869783fb1e80bda1625c844") + +if(CPM_SOURCE_CACHE) + set(CPM_DOWNLOAD_LOCATION "${CPM_SOURCE_CACHE}/cpm/CPM_${CPM_DOWNLOAD_VERSION}.cmake") +elseif(DEFINED ENV{CPM_SOURCE_CACHE}) + set(CPM_DOWNLOAD_LOCATION "$ENV{CPM_SOURCE_CACHE}/cpm/CPM_${CPM_DOWNLOAD_VERSION}.cmake") +else() + set(CPM_DOWNLOAD_LOCATION "${CMAKE_BINARY_DIR}/cmake/CPM_${CPM_DOWNLOAD_VERSION}.cmake") +endif() + +# Expand relative path. This is important if the provided path contains a tilde (~) +get_filename_component(CPM_DOWNLOAD_LOCATION ${CPM_DOWNLOAD_LOCATION} ABSOLUTE) + +file(DOWNLOAD + https://github.com/cpm-cmake/CPM.cmake/releases/download/v${CPM_DOWNLOAD_VERSION}/CPM.cmake + ${CPM_DOWNLOAD_LOCATION} EXPECTED_HASH SHA256=${CPM_HASH_SUM} +) + +include(${CPM_DOWNLOAD_LOCATION}) diff --git a/examples/dyna/data/gfx/Bomb.png b/examples/dyna/data/gfx/Bomb.png new file mode 100644 index 00000000..c070d2dd Binary files /dev/null and b/examples/dyna/data/gfx/Bomb.png differ diff --git a/examples/dyna/data/gfx/Player.png b/examples/dyna/data/gfx/Player.png new file mode 100644 index 00000000..cd8dbc6c Binary files /dev/null and b/examples/dyna/data/gfx/Player.png differ diff --git a/examples/dyna/data/gfx/bonusy.png b/examples/dyna/data/gfx/bonusy.png new file mode 100644 index 00000000..8810d149 Binary files /dev/null and b/examples/dyna/data/gfx/bonusy.png differ diff --git a/examples/dyna/data/gfx/crate.png b/examples/dyna/data/gfx/crate.png new file mode 100644 index 00000000..516be0c9 Binary files /dev/null and b/examples/dyna/data/gfx/crate.png differ diff --git a/examples/dyna/data/gfx/menu.png b/examples/dyna/data/gfx/menu.png new file mode 100644 index 00000000..46ce5019 Binary files /dev/null and b/examples/dyna/data/gfx/menu.png differ diff --git a/examples/dyna/data/gfx/monster1.png b/examples/dyna/data/gfx/monster1.png new file mode 100644 index 00000000..a9db4236 Binary files /dev/null and b/examples/dyna/data/gfx/monster1.png differ diff --git a/examples/dyna/data/gfx/monster2.png b/examples/dyna/data/gfx/monster2.png new file mode 100644 index 00000000..ab0a3135 Binary files /dev/null and b/examples/dyna/data/gfx/monster2.png differ diff --git a/examples/dyna/data/gfx/monster3.png b/examples/dyna/data/gfx/monster3.png new file mode 100644 index 00000000..e909e056 Binary files /dev/null and b/examples/dyna/data/gfx/monster3.png differ diff --git a/examples/dyna/data/gfx/portal.png b/examples/dyna/data/gfx/portal.png new file mode 100644 index 00000000..4d39c5d1 Binary files /dev/null and b/examples/dyna/data/gfx/portal.png differ diff --git a/examples/dyna/data/gfx/sand.png b/examples/dyna/data/gfx/sand.png new file mode 100644 index 00000000..612f5d22 Binary files /dev/null and b/examples/dyna/data/gfx/sand.png differ diff --git a/examples/dyna/data/gfx/wall.png b/examples/dyna/data/gfx/wall.png new file mode 100644 index 00000000..b975b965 Binary files /dev/null and b/examples/dyna/data/gfx/wall.png differ diff --git a/examples/dyna/data/levels/1 b/examples/dyna/data/levels/1 new file mode 100644 index 00000000..b5e5f44b --- /dev/null +++ b/examples/dyna/data/levels/1 @@ -0,0 +1,12 @@ +10 1 0 0 +@............ +.#.#.#.#.#.#. +............. +.#.#.#.#.#.#. +............. +.#.#.#.#.#.#. +............. +.#.#.#.#.#.#. +............. +.#.#.#.#.#.#. +............. diff --git a/examples/dyna/data/levels/2 b/examples/dyna/data/levels/2 new file mode 100644 index 00000000..908b5fed --- /dev/null +++ b/examples/dyna/data/levels/2 @@ -0,0 +1,12 @@ +20 4 0 0 +............. +.#.#.#.#.#.#. +............. +.#.#.#.#.#.#. +............. +.#.#.#@#.#.#. +............. +.#.#.#.#.#.#. +............. +.#.#.#.#.#.#. +............. diff --git a/examples/dyna/data/levels/3 b/examples/dyna/data/levels/3 new file mode 100644 index 00000000..b161424d --- /dev/null +++ b/examples/dyna/data/levels/3 @@ -0,0 +1,12 @@ +40 3 2 0 +@............ +.#.#.#.#.#.#. +............. +.#.#.#.#.#.#. +............. +.#.#.#.#.#.#. +............. +.#.#.#.#.#.#. +............. +.#.#.#.#.#.#. +............. diff --git a/examples/dyna/data/levels/4 b/examples/dyna/data/levels/4 new file mode 100644 index 00000000..a7f8730c --- /dev/null +++ b/examples/dyna/data/levels/4 @@ -0,0 +1,12 @@ +40 3 3 0 +@............ +.###.#.#.###. +............. +.#.#.#.#.#.#. +............. +.###.#.#.###. +............. +.#.#.#.#.#.#. +............. +.###.#.#.###. +............. diff --git a/examples/dyna/data/levels/5 b/examples/dyna/data/levels/5 new file mode 100644 index 00000000..ec7e3b15 --- /dev/null +++ b/examples/dyna/data/levels/5 @@ -0,0 +1,12 @@ +40 2 4 1 +@............ +.###.#.#.###. +.#.........#. +.#.#.#.#.#.#. +............. +.#.#.#.#.#.#. +............. +.#.#.#.#.#.#. +.#.........#. +.###.#.#.###. +............. diff --git a/examples/dyna/data/levels/6 b/examples/dyna/data/levels/6 new file mode 100644 index 00000000..74b7d3f3 --- /dev/null +++ b/examples/dyna/data/levels/6 @@ -0,0 +1,12 @@ +50 2 2 3 +.#.#.#.#.#.#. +............. +.#.#.#.#.#.#. +.....#.#..... +.#.###.###.#. +............. +.#.###.###.#. +.....#.#..... +.#.#.#.#.#.#. +............. +.#.#.#.#.#.#@ diff --git a/examples/dyna/data/levels/7 b/examples/dyna/data/levels/7 new file mode 100644 index 00000000..9e424997 --- /dev/null +++ b/examples/dyna/data/levels/7 @@ -0,0 +1,12 @@ +60 3 3 3 +@............ +.#.#.###.#.#. +............. +.###.#.#.###. +............. +.#.#.###.#.#. +............. +.###.#.#.###. +............. +.#.#.###.#.#. +............. diff --git a/examples/dyna/data/levels/8 b/examples/dyna/data/levels/8 new file mode 100644 index 00000000..b3f4f5e0 --- /dev/null +++ b/examples/dyna/data/levels/8 @@ -0,0 +1,12 @@ +60 5 3 3 +@............ +.#.#.#.#.#.#. +............. +.#...#.#...#. +............. +.#.#.#.#.#.#. +............. +.#...#.#...#. +............. +.#.#.#.#.#.#. +............. diff --git a/examples/dyna/data/levels/9 b/examples/dyna/data/levels/9 new file mode 100644 index 00000000..4036677f --- /dev/null +++ b/examples/dyna/data/levels/9 @@ -0,0 +1,12 @@ +90 5 5 5 +@............ +.#.........#. +............. +............. +............. +............. +............. +............. +............. +.#.........#. +............. diff --git a/examples/dyna/data/levels/menu b/examples/dyna/data/levels/menu new file mode 100644 index 00000000..ec586855 --- /dev/null +++ b/examples/dyna/data/levels/menu @@ -0,0 +1,12 @@ +30 4 4 4 +@............ +.#.#.#.#.#.#. +............. +.#...#.#...#. +............. +.#.#.#.#.#.#. +............. +.#...#.#...#. +............. +.#.#.#.#.#.#. +............. diff --git a/examples/dyna/src/bomb.cpp b/examples/dyna/src/bomb.cpp new file mode 100644 index 00000000..f820b1d1 --- /dev/null +++ b/examples/dyna/src/bomb.cpp @@ -0,0 +1,137 @@ +#include "bomb.hpp" + +#include "gfx.hpp" +#include "map.hpp" +#include "texture.hpp" +#include "timer.hpp" +#include "world.hpp" + +namespace dyna +{ + +Bomb::Bomb( int x_, int y_ ) + : x( x_ ) + , y( y_ ) + , left( 9 ) +{ +} + +void Bomb::draw() +{ + if( stage == Stage::exploding ) + return; + + if( stage == Stage::appear ) + { + Textures::bomb_appear.bind( 9 - left ); + } + else + { + int frame = static_cast( ( time - left ) / static_cast( time ) * 8 ); + if( Timer::get_timestamp() / 100 % 2 == 0 ) + frame++; + Textures::bomb.bind( frame ); + } + + Gfx::draw_square( x, y ); +} + +void Bomb::tick( World& world ) +{ + delta += Timer::delta; + + while( delta > 10 ) + { + delta -= 10; + + if( stage == Stage::appear ) + { + if( left > 0 ) + { + delta -= 10; // the fade-in advances at double speed + left--; + } + else + { + stage = Stage::ticking; + left = time; + } + } + else if( left > 0 ) + { + left--; + } + else if( stage == Stage::ticking ) + { + explode( world ); + } + else + { + die( world ); + } + } +} + +void Bomb::explode( World& world ) +{ + stage = Stage::exploding; + left = 200; + + Map& map = world.map(); + map.at( x, y ) = Field::explosion( Field::ExplosionType::center ); + + struct Dir + { + int dx, dy; + Field::ExplosionType through, tip; + }; + const Dir dirs[4] = { + { -1, 0, Field::ExplosionType::horizontal, Field::ExplosionType::left }, + { 1, 0, Field::ExplosionType::horizontal, Field::ExplosionType::right }, + { 0, -1, Field::ExplosionType::vertical, Field::ExplosionType::up }, + { 0, 1, Field::ExplosionType::vertical, Field::ExplosionType::down }, + }; + + for( const Dir& d : dirs ) + { + for( int i = 1; i <= maxrange; i++ ) + { + int tx = x + d.dx * i; + int ty = y + d.dy * i; + + if( tx < 0 || tx > map.getx() - 1 || ty < 0 || ty > map.gety() - 1 ) + break; + + Destruction destr = map.at( tx, ty ).destructible(); + if( destr == Destruction::none ) + break; + + etiles.emplace_back( tx, ty ); + + if( map.at( tx, ty ).kind == Field::Kind::crate ) + world.crates_left--; + + if( i == maxrange || destr == Destruction::single ) + { + map.at( tx, ty ) = Field::explosion( d.tip ); + break; + } + else + { + map.at( tx, ty ) = Field::explosion( d.through ); + } + } + } +} + +void Bomb::die( World& world ) +{ + dead = true; + + Map& map = world.map(); + map.at( x, y ) = Field::floor(); + for( const auto& [tx, ty] : etiles ) + map.at( tx, ty ) = Field::floor(); +} + +} diff --git a/examples/dyna/src/bomb.hpp b/examples/dyna/src/bomb.hpp new file mode 100644 index 00000000..f9dc0a0d --- /dev/null +++ b/examples/dyna/src/bomb.hpp @@ -0,0 +1,44 @@ +#pragma once + +#include +#include + +namespace dyna +{ + +class World; + +// A bomb on the grid: fades in, counts down, then paints a cross-shaped +// explosion onto the map and clears it again. Ported from bomb.cs. +class Bomb +{ +public: + Bomb( int x, int y ); + + void draw(); + void tick( World& world ); + + bool is_dead() const { return dead; } + +private: + void explode( World& world ); + void die( World& world ); + + enum class Stage + { + appear, + ticking, + exploding + }; + + int x, y; // grid coordinates + Stage stage = Stage::appear; + int left; + int delta = 0; + static constexpr int time = 150; + static constexpr int maxrange = 1; + std::vector> etiles; // tiles to revert to floor + bool dead = false; +}; + +} diff --git a/examples/dyna/src/bonus.cpp b/examples/dyna/src/bonus.cpp new file mode 100644 index 00000000..f61b5f4d --- /dev/null +++ b/examples/dyna/src/bonus.cpp @@ -0,0 +1,54 @@ +#include "bonus.hpp" + +#include "gfx.hpp" +#include "texture.hpp" +#include "timer.hpp" + +namespace dyna +{ + +Vortex::Vortex( int gx, int gy ) +{ + x = gx; // stored in grid units, drawn via draw_square + y = gy; + set_action( Action::appear ); + left = 79; +} + +void Vortex::draw() +{ + int frame = static_cast( ( Timer::get_timestamp() - action_start ) / 40 ); + + switch( action ) + { + case Action::appear: + Textures::vortex_appear.bind( frame ); + break; + case Action::wait: + Textures::vortex.bind( frame ); + break; + default: + break; + } + + Gfx::draw_square( x, y ); +} + +void Vortex::tick( World& ) +{ + delta += Timer::delta; + + while( delta > 10 ) + { + delta -= 10; + + if( left > 0 ) + left--; + else if( action == Action::appear ) + set_action( Action::wait ); + } +} + +void Vortex::die( World& ) {} + +} diff --git a/examples/dyna/src/bonus.hpp b/examples/dyna/src/bonus.hpp new file mode 100644 index 00000000..d27ceb55 --- /dev/null +++ b/examples/dyna/src/bonus.hpp @@ -0,0 +1,20 @@ +#pragma once + +#include "entity.hpp" + +namespace dyna +{ + +// The level-exit portal. Unlike the other entities its coordinates are stored in +// grid units (it draws via draw_square), matching bonus.cs. +class Vortex : public Entity +{ +public: + Vortex( int gx, int gy ); + + void draw() override; + void tick( World& world ) override; + void die( World& world ) override; +}; + +} diff --git a/examples/dyna/src/datapath.cpp b/examples/dyna/src/datapath.cpp new file mode 100644 index 00000000..76f253f0 --- /dev/null +++ b/examples/dyna/src/datapath.cpp @@ -0,0 +1,20 @@ +#include "datapath.hpp" + +#include + +namespace dyna +{ + +std::string data_path( const std::string& rel ) +{ + // SDL_GetBasePath returns the executable's directory (with a trailing + // separator) and is owned by SDL, so cache it for the program's lifetime. + static const std::string base = [] + { + const char* p = SDL_GetBasePath(); + return std::string( p ? p : "" ); + }(); + return base + rel; +} + +} diff --git a/examples/dyna/src/datapath.hpp b/examples/dyna/src/datapath.hpp new file mode 100644 index 00000000..1f12fac8 --- /dev/null +++ b/examples/dyna/src/datapath.hpp @@ -0,0 +1,14 @@ +#pragma once + +#include + +namespace dyna +{ + +// Resolve a path relative to the directory containing the executable, so the +// game finds its data files regardless of the current working directory (e.g. +// when launched from the build tree). The data/ tree is copied next to the +// binary at build time; see CMakeLists.txt. +std::string data_path( const std::string& rel ); + +} diff --git a/examples/dyna/src/entity.cpp b/examples/dyna/src/entity.cpp new file mode 100644 index 00000000..616c06a3 --- /dev/null +++ b/examples/dyna/src/entity.cpp @@ -0,0 +1,39 @@ +#include "entity.hpp" + +#include "map.hpp" +#include "timer.hpp" + +namespace dyna +{ + +void Entity::set_action( Action a ) +{ + action = a; + action_start = Timer::get_timestamp(); +} + +bool Entity::can_move( Action a, const Map& map ) const +{ + switch( a ) + { + case Action::up: + return y > 0 && !map.at( x / 64, y / 64 - 1 ).solid(); + case Action::down: + return y / 64 < map.gety() - 1 && !map.at( x / 64, y / 64 + 1 ).solid(); + case Action::left: + return x > 0 && !map.at( x / 64 - 1, y / 64 ).solid(); + case Action::right: + return x / 64 < map.getx() - 1 && !map.at( x / 64 + 1, y / 64 ).solid(); + default: + return true; + } +} + +bool Entity::killed( const Map& map ) const +{ + int tx = ( x + 32 ) / 64; + int ty = ( y + 32 ) / 64; + return map.at( tx, ty ).kind == Field::Kind::explosion; +} + +} diff --git a/examples/dyna/src/entity.hpp b/examples/dyna/src/entity.hpp new file mode 100644 index 00000000..686f67ac --- /dev/null +++ b/examples/dyna/src/entity.hpp @@ -0,0 +1,52 @@ +#pragma once + +#include + +namespace dyna +{ + +class Map; +class World; + +// Movement/state verbs shared by the player and monsters. In the C# source this +// lived as Entity.Action; promoted to namespace scope so Game can refer to it. +enum class Action +{ + wait, + up, + down, + left, + right, + death, + place_bomb, + appear +}; + +// Base for everything that moves on the grid. Coordinates are in pixels +// (64 per tile) and laid out top-left origin, matching entity.cs. +class Entity +{ +public: + virtual ~Entity() = default; + + virtual void set_action( Action a ); + + int getx() const { return x; } + int gety() const { return y; } + + virtual void draw() = 0; + virtual void tick( World& world ) = 0; + virtual void die( World& world ) = 0; + +protected: + bool can_move( Action a, const Map& map ) const; + virtual bool killed( const Map& map ) const; + + int x = 0, y = 0; + std::int64_t action_start = 0; + int delta = 0; + Action action = Action::wait; + int left = 0; +}; + +} diff --git a/examples/dyna/src/game.cpp b/examples/dyna/src/game.cpp new file mode 100644 index 00000000..492a1c03 --- /dev/null +++ b/examples/dyna/src/game.cpp @@ -0,0 +1,177 @@ +#include "game.hpp" + +#include "datapath.hpp" +#include "gfx.hpp" +#include "map.hpp" +#include "player.hpp" +#include "timer.hpp" +#include "world.hpp" + +#include + +#include + +namespace dyna +{ + +namespace Game +{ + +namespace +{ + +SDL_Keycode key = 0; // most recently pressed movement key +bool help = false; + +// Run one level to completion. Returns true if the player asked to quit the +// whole application (window close), false if the level simply ended (death, +// escape, or reaching the exit) and control should return to the caller. +bool level_loop( World& world ) +{ + Player* p = world.player(); + + for( ;; ) + { + SDL_Event ev; + while( SDL_PollEvent( &ev ) ) + { + if( ev.type == SDL_EVENT_QUIT ) + return true; + + if( ev.type == SDL_EVENT_KEY_DOWN && !ev.key.repeat ) + { + switch( ev.key.key ) + { + case SDLK_ESCAPE: + world.killed = true; + return false; + case SDLK_LEFT: + key = SDLK_LEFT; + p->move( Action::left ); + break; + case SDLK_RIGHT: + key = SDLK_RIGHT; + p->move( Action::right ); + break; + case SDLK_UP: + key = SDLK_UP; + p->move( Action::up ); + break; + case SDLK_DOWN: + key = SDLK_DOWN; + p->move( Action::down ); + break; + case SDLK_SPACE: + world.map().place_bomb( ( p->getx() + 32 ) / 64, ( p->gety() + 32 ) / 64 ); + break; + default: + break; + } + } + + if( ev.type == SDL_EVENT_KEY_UP ) + { + switch( ev.key.key ) + { + case SDLK_LEFT: + if( key == SDLK_LEFT ) p->move( Action::wait ); + break; + case SDLK_RIGHT: + if( key == SDLK_RIGHT ) p->move( Action::wait ); + break; + case SDLK_UP: + if( key == SDLK_UP ) p->move( Action::wait ); + break; + case SDLK_DOWN: + if( key == SDLK_DOWN ) p->move( Action::wait ); + break; + default: + break; + } + } + } + + Gfx::clear(); + + Timer::tick(); + + world.tick(); + world.draw(); + + Gfx::swap(); + + if( world.killed || world.next_level ) + return false; + } +} + +// Play through the levels in order. Returns true if the application should quit. +bool new_game() +{ + int level = 1; + + for( ;; ) + { + World world( data_path( "data/levels/" + std::to_string( level ) ), true ); + + if( level_loop( world ) ) + return true; // window closed + + if( world.killed ) + return false; // died or escaped to the menu + if( ++level >= 10 ) + return false; // cleared the last level + } +} + +} // namespace + +void menu_loop() +{ + World world( data_path( "data/levels/menu" ), false ); + + for( ;; ) + { + SDL_Event ev; + while( SDL_PollEvent( &ev ) ) + { + if( ev.type == SDL_EVENT_QUIT ) + return; + + if( ev.type == SDL_EVENT_KEY_DOWN && !ev.key.repeat ) + { + switch( ev.key.key ) + { + case SDLK_ESCAPE: + return; + case SDLK_SPACE: + if( new_game() ) + return; // window closed during play + break; + case SDLK_H: + help = !help; + break; + default: + break; + } + } + } + + Gfx::clear(); + + Timer::tick(); + world.tick(); + world.draw(); + + if( help ) + Gfx::show_help(); + else + Gfx::show_menu(); + + Gfx::swap(); + } +} + +} // namespace Game + +} diff --git a/examples/dyna/src/game.hpp b/examples/dyna/src/game.hpp new file mode 100644 index 00000000..8a833d53 --- /dev/null +++ b/examples/dyna/src/game.hpp @@ -0,0 +1,14 @@ +#pragma once + +namespace dyna +{ + +// Top-level game flow, ported from game.cs. The C# original kept the running +// game's state (player, map, win/lose flags) in static fields; that state now +// lives in a World object owned by the loops below, so nothing leaks out here. +namespace Game +{ +void menu_loop(); +} + +} diff --git a/examples/dyna/src/gfx.cpp b/examples/dyna/src/gfx.cpp new file mode 100644 index 00000000..29f024d7 --- /dev/null +++ b/examples/dyna/src/gfx.cpp @@ -0,0 +1,412 @@ +#include "gfx.hpp" + +#include "texture.hpp" +#include "timer.hpp" + +#include + +#include +#include + +namespace dyna +{ + +namespace +{ + +SDL_Window* g_window = nullptr; +SDL_GLContext g_gl_context = nullptr; + +GLuint g_program = 0; +GLuint g_vao = 0; +GLuint g_vbo = 0; + +// Current draw state, applied to every quad appended to the batch. +GLuint g_current_tex = 0; +int g_current_layer = 0; +float g_alpha = 1.0f; + +// One vertex of the streaming batch: screen position, atlas-array texcoord, +// the array layer to sample and a per-vertex alpha multiplier. +struct GlVert +{ + float px, py, tx, ty, layer, a; +}; + +// A run of consecutive vertices that share one texture, drawn in a single call. +struct DrawCmd +{ + GLuint tex; + GLsizei count; +}; + +std::vector g_verts; +std::vector g_cmds; + +const char* VERT_SRC = R"( +#version 330 core +uniform mat4 uProjection; +layout(location = 0) in vec2 aPosition; +layout(location = 1) in vec2 aTexCoord; +layout(location = 2) in float aLayer; +layout(location = 3) in float aAlpha; +out vec3 vTexCoord; +out float vAlpha; +void main() { + gl_Position = uProjection * vec4(aPosition, 0.0, 1.0); + vTexCoord = vec3(aTexCoord, aLayer); + vAlpha = aAlpha; +} +)"; + +const char* FRAG_SRC = R"( +#version 330 core +uniform sampler2DArray uTexture; +in vec3 vTexCoord; +in float vAlpha; +out vec4 fragColor; +void main() { + fragColor = texture(uTexture, vTexCoord) * vec4(1.0, 1.0, 1.0, vAlpha); +} +)"; + +GLuint compile_shader( GLenum type, const char* src ) +{ + GLuint s = glCreateShader( type ); + glShaderSource( s, 1, &src, nullptr ); + glCompileShader( s ); + GLint ok = 0; + glGetShaderiv( s, GL_COMPILE_STATUS, &ok ); + if( !ok ) + { + char log[512]; + glGetShaderInfoLog( s, 512, nullptr, log ); + std::fprintf( stderr, "Shader compile error: %s\n", log ); + glDeleteShader( s ); + return 0; + } + return s; +} + +bool init_shaders() +{ + GLuint vs = compile_shader( GL_VERTEX_SHADER, VERT_SRC ); + if( !vs ) return false; + GLuint fs = compile_shader( GL_FRAGMENT_SHADER, FRAG_SRC ); + if( !fs ) + { + glDeleteShader( vs ); + return false; + } + + g_program = glCreateProgram(); + glAttachShader( g_program, vs ); + glAttachShader( g_program, fs ); + glLinkProgram( g_program ); + glDeleteShader( vs ); + glDeleteShader( fs ); + + GLint ok = 0; + glGetProgramiv( g_program, GL_LINK_STATUS, &ok ); + if( !ok ) + { + char log[512]; + glGetProgramInfoLog( g_program, 512, nullptr, log ); + std::fprintf( stderr, "Program link error: %s\n", log ); + glDeleteProgram( g_program ); + g_program = 0; + return false; + } + + // Bottom-left origin orthographic projection, matching the original + // gluOrtho2D(0, w, 0, h) so the ported draw code carries over verbatim. + float l = 0.0f, r = static_cast( Gfx::w ); + float b = 0.0f, t = static_cast( Gfx::h ); + float proj[16] = { + 2.0f / ( r - l ), 0.0f, 0.0f, 0.0f, + 0.0f, 2.0f / ( t - b ), 0.0f, 0.0f, + 0.0f, 0.0f, -1.0f, 0.0f, + -( r + l ) / ( r - l ), -( t + b ) / ( t - b ), 0.0f, 1.0f }; + + glUseProgram( g_program ); + glUniformMatrix4fv( glGetUniformLocation( g_program, "uProjection" ), 1, GL_FALSE, proj ); + glUniform1i( glGetUniformLocation( g_program, "uTexture" ), 0 ); + glUseProgram( 0 ); + return true; +} + +void init_quad_vao() +{ + glGenVertexArrays( 1, &g_vao ); + glGenBuffers( 1, &g_vbo ); + + glBindVertexArray( g_vao ); + glBindBuffer( GL_ARRAY_BUFFER, g_vbo ); + + const GLsizei stride = sizeof( GlVert ); + glEnableVertexAttribArray( 0 ); + glVertexAttribPointer( 0, 2, GL_FLOAT, GL_FALSE, stride, (void*)0 ); + glEnableVertexAttribArray( 1 ); + glVertexAttribPointer( 1, 2, GL_FLOAT, GL_FALSE, stride, (void*)8 ); + glEnableVertexAttribArray( 2 ); + glVertexAttribPointer( 2, 1, GL_FLOAT, GL_FALSE, stride, (void*)16 ); + glEnableVertexAttribArray( 3 ); + glVertexAttribPointer( 3, 1, GL_FLOAT, GL_FALSE, stride, (void*)20 ); + + glBindVertexArray( 0 ); + glBindBuffer( GL_ARRAY_BUFFER, 0 ); +} + +// Draw and clear everything accumulated since the last flush, in submission +// order. Consecutive quads that share a texture collapse into one draw call. +void flush_batch() +{ + if( g_verts.empty() ) + return; + + glBindBuffer( GL_ARRAY_BUFFER, g_vbo ); + glBufferData( GL_ARRAY_BUFFER, + static_cast( g_verts.size() * sizeof( GlVert ) ), + g_verts.data(), GL_STREAM_DRAW ); + + glUseProgram( g_program ); + glBindVertexArray( g_vao ); + + GLint offset = 0; + for( const DrawCmd& cmd : g_cmds ) + { + glBindTexture( GL_TEXTURE_2D_ARRAY, cmd.tex ); + glDrawArrays( GL_TRIANGLES, offset, cmd.count ); + offset += cmd.count; + } + + glBindVertexArray( 0 ); + glUseProgram( 0 ); + glBindBuffer( GL_ARRAY_BUFFER, 0 ); + + g_verts.clear(); + g_cmds.clear(); +} + +} // namespace + +namespace Render +{ + +bool init() +{ + if( !init_shaders() ) return false; + init_quad_vao(); + return true; +} + +void shutdown() +{ + if( g_vbo ) glDeleteBuffers( 1, &g_vbo ); + if( g_vao ) glDeleteVertexArrays( 1, &g_vao ); + if( g_program ) glDeleteProgram( g_program ); + g_vbo = g_vao = g_program = 0; +} + +void use_texture( GLuint tex, int layer ) +{ + g_current_tex = tex; + g_current_layer = layer; +} + +GLuint make_texture( int w, int h, int layers, const void* rgba ) +{ + GLuint tex = 0; + glGenTextures( 1, &tex ); + glBindTexture( GL_TEXTURE_2D_ARRAY, tex ); + glTexParameteri( GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MIN_FILTER, GL_NEAREST ); + glTexParameteri( GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MAG_FILTER, GL_NEAREST ); + glTexParameteri( GL_TEXTURE_2D_ARRAY, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE ); + glTexParameteri( GL_TEXTURE_2D_ARRAY, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE ); + glPixelStorei( GL_UNPACK_ALIGNMENT, 1 ); + glTexImage3D( GL_TEXTURE_2D_ARRAY, 0, GL_RGBA, w, h, layers, 0, GL_RGBA, GL_UNSIGNED_BYTE, rgba ); + return tex; +} + +} // namespace Render + +namespace Gfx +{ + +void clear() +{ + glClear( GL_COLOR_BUFFER_BIT ); +} + +void swap() +{ + flush_batch(); + SDL_GL_SwapWindow( g_window ); +} + +void alpha( float a ) +{ + g_alpha = a; +} + +void draw_quad( const Vertex corners[4] ) +{ + // Two triangles, vertices appended in submission order so painter ordering + // (and the transient per-monster alpha) is preserved by the batch. + const int idx[6] = { 0, 1, 2, 0, 2, 3 }; + for( int i : idx ) + { + const Vertex& c = corners[i]; + g_verts.push_back( { c.x, c.y, c.u, c.v, + static_cast( g_current_layer ), g_alpha } ); + } + + if( !g_cmds.empty() && g_cmds.back().tex == g_current_tex ) + g_cmds.back().count += 6; + else + g_cmds.push_back( { g_current_tex, 6 } ); +} + +void draw_sprite( int x, int y ) +{ + float fx = static_cast( x ); + float fy = static_cast( y ); + float top = static_cast( h ) - fy; + float bottom = static_cast( h ) - ( fy + 64.0f ); + Vertex corners[4] = { + { fx, top, 0.0f, 0.0f }, + { fx + 64.0f, top, 1.0f, 0.0f }, + { fx + 64.0f, bottom, 1.0f, 1.0f }, + { fx, bottom, 0.0f, 1.0f }, + }; + draw_quad( corners ); +} + +void draw_square( int x, int y ) +{ + draw_sprite( x * 64, y * 64 ); +} + +void show_help() +{ + Textures::menu.bind(); + + const float fw = static_cast( w ); + const float fh = static_cast( h ); + Vertex bg[4] = { + { 0.0f, fh, 0.0f, 0.0f }, + { fw, fh, 832.0f / 1024, 0.0f }, + { fw, 0.0f, 832.0f / 1024, 704.0f / 1024 }, + { 0.0f, 0.0f, 0.0f, 704.0f / 1024 }, + }; + draw_quad( bg ); + + int t = static_cast( Timer::get_timestamp() / 40 ); + + Textures::p_r.bind( t ); + draw_sprite( 150, 85 ); + Textures::m1_r.bind( t ); + draw_sprite( 75, 160 ); + Textures::m2_r.bind( t ); + draw_sprite( 150, 160 ); + Textures::m3_r.bind( t ); + draw_sprite( 225, 160 ); + Textures::bomb.bind( static_cast( Timer::get_timestamp() / 100 % 2 ) ); + draw_sprite( 150, 235 ); + Textures::wall.bind(); + draw_sprite( 150, 310 ); + Textures::crate.bind(); + draw_sprite( 150, 385 ); + Textures::vortex.bind( t ); + draw_sprite( 150, 460 ); + Textures::bonus1.bind( t ); + draw_sprite( 112, 535 ); + Textures::bonus2.bind( t ); + draw_sprite( 187, 535 ); +} + +void show_menu() +{ + Textures::menu.bind(); + + Vertex logo[4] = { + { float( ( w - 594 ) / 2 ), float( h - 50 ), 1.0f, 0.0f }, + { float( ( w + 594 ) / 2 ), float( h - 50 ), 1.0f, 594.0f / 1024 }, + { float( ( w + 594 ) / 2 ), float( h - 50 - 180 ), 1.0f - 180.0f / 1024, 594.0f / 1024 }, + { float( ( w - 594 ) / 2 ), float( h - 50 - 180 ), 1.0f - 180.0f / 1024, 0.0f }, + }; + draw_quad( logo ); + + Vertex prompt[4] = { + { float( ( w - 527 ) / 2 ), 335.0f, 0.0f, 704.0f / 1024 }, + { float( ( w + 527 ) / 2 ), 335.0f, 527.0f / 1024, 704.0f / 1024 }, + { float( ( w + 527 ) / 2 ), 20.0f, 527.0f / 1024, 1019.0f / 1024 }, + { float( ( w - 527 ) / 2 ), 20.0f, 0.0f, 1019.0f / 1024 }, + }; + draw_quad( prompt ); +} + +} // namespace Gfx + +namespace Init +{ + +bool all() +{ + if( !SDL_Init( SDL_INIT_VIDEO ) ) + { + std::fprintf( stderr, "SDL_Init failed: %s\n", SDL_GetError() ); + return false; + } + + SDL_GL_SetAttribute( SDL_GL_DOUBLEBUFFER, 1 ); + SDL_GL_SetAttribute( SDL_GL_CONTEXT_MAJOR_VERSION, 3 ); + SDL_GL_SetAttribute( SDL_GL_CONTEXT_MINOR_VERSION, 3 ); + SDL_GL_SetAttribute( SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE ); + + g_window = SDL_CreateWindow( "Dyna.net", Gfx::w, Gfx::h, SDL_WINDOW_OPENGL ); + if( !g_window ) + { + std::fprintf( stderr, "SDL_CreateWindow failed: %s\n", SDL_GetError() ); + return false; + } + + g_gl_context = SDL_GL_CreateContext( g_window ); + if( !g_gl_context ) + { + std::fprintf( stderr, "SDL_GL_CreateContext failed: %s\n", SDL_GetError() ); + return false; + } + + int version = gladLoadGL( (GLADloadfunc)SDL_GL_GetProcAddress ); + if( version == 0 ) + { + std::fprintf( stderr, "gladLoadGL failed\n" ); + return false; + } + + SDL_GL_SetSwapInterval( 1 ); // vsync; the game is time-based so speed is unaffected + + glViewport( 0, 0, Gfx::w, Gfx::h ); + glClearColor( 0.0f, 0.0f, 0.0f, 1.0f ); + glEnable( GL_BLEND ); + glBlendFunc( GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA ); + + if( !Render::init() ) return false; + + Timer::reset(); + Textures::preload(); + return true; +} + +void shutdown() +{ + Render::shutdown(); + if( g_gl_context ) SDL_GL_DestroyContext( g_gl_context ); + if( g_window ) SDL_DestroyWindow( g_window ); + SDL_Quit(); +} + +} // namespace Init + +} diff --git a/examples/dyna/src/gfx.hpp b/examples/dyna/src/gfx.hpp new file mode 100644 index 00000000..28b519a1 --- /dev/null +++ b/examples/dyna/src/gfx.hpp @@ -0,0 +1,59 @@ +#pragma once + +#include + +namespace dyna +{ + +// Screen dimensions, matching the original 13x11 grid of 64px tiles. +namespace Gfx +{ +constexpr int w = 832; +constexpr int h = 704; + +void clear(); +void swap(); + +// Drawing primitives ported from gfx.cs. They render with the currently +// bound texture (see Texture::bind) and the current alpha. The coordinate +// system is bottom-left origin with y growing upward, exactly as the C# +// gluOrtho2D setup; draw_sprite/draw_square take y measured from the top +// and flip internally, so game-side coordinates stay top-left based. +void alpha( float a ); +void draw_sprite( int x, int y ); // pixel position of the top-left corner +void draw_square( int x, int y ); // grid position (multiplied by 64) + +// A single textured quad given four explicit (position, texcoord) corners, +// used by the menu/help screens which sample rotated regions of the atlas. +struct Vertex +{ + float x, y, u, v; +}; +void draw_quad( const Vertex corners[4] ); + +void show_help(); +void show_menu(); +} + +// Renderer back end shared by the texture loaders. +namespace Render +{ +bool init(); // shaders + streaming VBO/VAO +void shutdown(); // delete the program and buffers + +// Select the array texture (and layer within it) used by subsequent draws. +void use_texture( GLuint tex, int layer ); + +// Upload `layers` tightly packed RGBA8 images of size w*h as one +// GL_TEXTURE_2D_ARRAY and return its name (0 on failure). +GLuint make_texture( int w, int h, int layers, const void* rgba ); +} + +// One-time startup/shutdown, ported from the Init class in gfx.cs. +namespace Init +{ +bool all(); // SDL, GL context, renderer, textures, timer +void shutdown(); +} + +} diff --git a/examples/dyna/src/main.cpp b/examples/dyna/src/main.cpp new file mode 100644 index 00000000..3fb6bece --- /dev/null +++ b/examples/dyna/src/main.cpp @@ -0,0 +1,15 @@ +#include "game.hpp" +#include "gfx.hpp" + +#include + +int main( int /*argc*/, char* /*argv*/[] ) +{ + if( !dyna::Init::all() ) + return 1; + + dyna::Game::menu_loop(); + + dyna::Init::shutdown(); + return 0; +} diff --git a/examples/dyna/src/map.cpp b/examples/dyna/src/map.cpp new file mode 100644 index 00000000..5bfc2c3d --- /dev/null +++ b/examples/dyna/src/map.cpp @@ -0,0 +1,324 @@ +#include "map.hpp" + +#include "bomb.hpp" +#include "bonus.hpp" +#include "gfx.hpp" +#include "monster.hpp" +#include "player.hpp" +#include "texture.hpp" +#include "timer.hpp" +#include "world.hpp" + +#include +#include +#include +#include +#include + +namespace dyna +{ + +// ---- Field -------------------------------------------------------------- + +Field Field::explosion( ExplosionType t ) +{ + Field f; + f.kind = Kind::explosion; + f.etype = t; + f.tstart = Timer::get_timestamp(); + return f; +} + +bool Field::solid() const +{ + switch( kind ) + { + case Kind::wall: + case Kind::crate: + case Kind::bomb: + return true; + default: + return false; + } +} + +Destruction Field::destructible() const +{ + switch( kind ) + { + case Kind::floor: + return Destruction::multi; + case Kind::crate: + return Destruction::single; + default: + return Destruction::none; + } +} + +void Field::draw( int x, int y ) const +{ + switch( kind ) + { + case Kind::wall: + Textures::wall.bind(); + Gfx::draw_square( x, y ); + break; + + case Kind::crate: + Textures::sand.bind(); + Gfx::draw_square( x, y ); + Textures::crate.bind(); + Gfx::draw_square( x, y ); + break; + + case Kind::explosion: { + Textures::sand.bind(); + Gfx::draw_square( x, y ); + + int frame = static_cast( ( Timer::get_timestamp() - tstart ) / 40 % 8 ); + if( frame > 4 ) frame = 8 - frame; + + switch( etype ) + { + case ExplosionType::center: Textures::e_c.bind( frame ); break; + case ExplosionType::vertical: Textures::e_v.bind( frame ); break; + case ExplosionType::horizontal: Textures::e_h.bind( frame ); break; + case ExplosionType::left: Textures::e_le.bind( frame ); break; + case ExplosionType::right: Textures::e_re.bind( frame ); break; + case ExplosionType::up: Textures::e_ue.bind( frame ); break; + case ExplosionType::down: Textures::e_de.bind( frame ); break; + } + Gfx::draw_square( x, y ); + break; + } + + // floor, bomb and vortex tiles all show plain sand; the bomb and vortex + // sprites themselves are drawn by their entities. + case Kind::floor: + case Kind::bomb: + case Kind::vortex: + default: + Textures::sand.bind(); + Gfx::draw_square( x, y ); + break; + } +} + +// ---- Map ---------------------------------------------------------------- + +Map::Map( const std::string& fn ) +{ + load( fn ); + generate_destructibles(); + populate_map(); +} + +Map::~Map() = default; + +void Map::load( const std::string& fn ) +{ + std::ifstream f( fn ); + if( !f ) + { + std::fprintf( stderr, "Cannot open level %s\n", fn.c_str() ); + grid.assign( X * Y, Field::floor() ); + return; + } + + std::stringstream buf; + buf << f.rdbuf(); + std::string content = buf.str(); + + size_t nl = content.find( '\n' ); + std::string header = ( nl == std::string::npos ) ? content : content.substr( 0, nl ); + std::sscanf( header.c_str(), "%d %d %d %d", &destructibles, &m1, &m2, &m3 ); + + grid.assign( X * Y, Field::floor() ); + px = -1; + + size_t p = ( nl == std::string::npos ) ? content.size() : nl + 1; + for( int ry = 0; ry < Y; ry++ ) + { + for( int rx = 0; rx < X; rx++ ) + { + char c = ( p < content.size() ) ? content[p++] : '\0'; + switch( c ) + { + case '.': + at( rx, ry ) = Field::floor(); + break; + case '#': + at( rx, ry ) = Field::wall(); + break; + case '@': + at( rx, ry ) = Field::floor(); + px = rx; + py = ry; + break; + case '\n': + rx--; // newlines don't consume a grid cell + break; + default: + break; + } + } + } +} + +bool Map::monster_ok( int rx, int ry, int pxx, int pyy, int r ) const +{ + const Field& f = at( rx, ry ); + return f.is_floor_family() && f.kind != Field::Kind::crate && + ( std::abs( rx - pxx ) > r || std::abs( ry - pyy ) > r ); +} + +void Map::generate_destructibles() +{ + int i = destructibles; + while( i != 0 ) + { + int rx = RNG::next( X ); + int ry = RNG::next( Y ); + if( monster_ok( rx, ry, px, py, 1 ) ) + { + at( rx, ry ) = Field::crate(); + i--; + } + } +} + +void Map::populate_map() +{ + for( int type = 1; type <= 3; type++ ) + { + int count = ( type == 1 ) ? m1 : ( type == 2 ) ? m2 + : m3; + while( count != 0 ) + { + int rx = RNG::next( X ); + int ry = RNG::next( Y ); + if( monster_ok( rx, ry, px, py, 2 ) ) + { + monsters.push_back( std::make_unique( type, rx, ry ) ); + count--; + } + } + } +} + +void Map::draw() +{ + for( int ry = 0; ry < Y; ry++ ) + for( int rx = 0; rx < X; rx++ ) + at( rx, ry ).draw( rx, ry ); + + for( auto& b : bombs ) b->draw(); + for( auto& e : monsters ) e->draw(); + for( auto& e : bonuses ) e->draw(); +} + +void Map::tick( World& world ) +{ + // Bombs. + for( auto& b : bombs ) b->tick( world ); + bombs.erase( std::remove_if( bombs.begin(), bombs.end(), + []( const std::unique_ptr& b ) { return b->is_dead(); } ), + bombs.end() ); + + // Monsters: tick, then retire the dead and queue their respawn timers. + for( auto& e : monsters ) e->tick( world ); + for( auto& e : monsters ) + { + if( e->is_dead() ) + { + int delay = ( e->type() == 1 ) ? 10000 : ( e->type() == 2 ) ? 20000 + : 30000; + mwait.push_back( { e->type(), Timer::get_timestamp() + delay } ); + } + } + monsters.erase( std::remove_if( monsters.begin(), monsters.end(), + []( const std::unique_ptr& e ) { return e->is_dead(); } ), + monsters.end() ); + + // The respawn and exit-portal placement below need the player's position; + // they only fire during gameplay (a monster died, or every crate is gone), + // never on the player-less menu screen. + Player* player = world.player(); + + // Respawn monsters whose wait has elapsed. + std::int64_t now = Timer::get_timestamp(); + std::vector still_waiting; + for( const MWait& m : mwait ) + { + if( m.time < now && player ) + { + int rx = 0, ry = 0; + bool ok = false; + while( !ok ) + { + rx = RNG::next( X ); + ry = RNG::next( Y ); + if( monster_ok( rx, ry, player->getx() / 64, player->gety() / 64, 3 ) ) + ok = true; + } + auto monster = std::make_unique( m.type, rx, ry ); + monster->set_action( Action::appear ); + monsters.push_back( std::move( monster ) ); + } + else + { + still_waiting.push_back( m ); + } + } + mwait = std::move( still_waiting ); + + // Bonuses. + for( auto& e : bonuses ) e->tick( world ); + + // Once every crate is gone, open the exit portal somewhere clear. + if( world.crates_left == 0 && player ) + { + world.crates_left--; + + int rx = 0, ry = 0; + bool ok = false; + while( !ok ) + { + rx = RNG::next( X ); + ry = RNG::next( Y ); + if( monster_ok( rx, ry, player->getx() / 64, player->gety() / 64, 4 ) ) + ok = true; + } + + at( rx, ry ) = Field::vortex(); + bonuses.push_back( std::make_unique( rx, ry ) ); + } +} + +std::unique_ptr Map::create_player() const +{ + return std::make_unique( px, py ); +} + +void Map::place_bomb( int x, int y ) +{ + Field& f = at( x, y ); + if( f.is_floor_family() && f.kind != Field::Kind::bomb ) + { + f = Field::bomb(); + bombs.push_back( std::make_unique( x, y ) ); + } +} + +bool Map::monster_collide( int tx, int ty ) const +{ + for( const auto& e : monsters ) + { + if( ( e->getx() + 32 ) / 64 == ( tx + 32 ) / 64 && + ( e->gety() + 32 ) / 64 == ( ty + 32 ) / 64 ) + return true; + } + return false; +} + +} diff --git a/examples/dyna/src/map.hpp b/examples/dyna/src/map.hpp new file mode 100644 index 00000000..9a3f3451 --- /dev/null +++ b/examples/dyna/src/map.hpp @@ -0,0 +1,120 @@ +#pragma once + +#include +#include +#include +#include + +namespace dyna +{ + +class Player; +class Bomb; +class Monster; +class Vortex; +class World; + +// How a tile reacts to an explosion sweeping through it. +enum class Destruction +{ + none, // blocks the blast (wall, bomb, existing explosion, vortex) + single, // destroyed and stops the blast (crate) + multi // passable, blast continues (floor) +}; + +// A grid cell. The C# version used a small class hierarchy rooted at a Field +// interface; since the variants differ only in a couple of flags and how they +// draw, this collapses them into one value type tagged by Kind. Note that in +// the original everything except Wall derived from Floor, so the "is Floor" +// checks there map to "kind != Wall" here. +struct Field +{ + enum class Kind + { + floor, + wall, + crate, + bomb, // tile occupied by a live bomb (solid, indestructible) + explosion, // transient blast tile + vortex // level exit portal + }; + + enum class ExplosionType + { + center, + vertical, + horizontal, + left, + right, + down, + up + }; + + Kind kind = Kind::floor; + ExplosionType etype = ExplosionType::center; + std::int64_t tstart = 0; // explosion animation start, set on creation + + static Field floor() { return Field{}; } + static Field wall() { return Field{ Kind::wall, {}, 0 }; } + static Field crate() { return Field{ Kind::crate, {}, 0 }; } + static Field bomb() { return Field{ Kind::bomb, {}, 0 }; } + static Field vortex() { return Field{ Kind::vortex, {}, 0 }; } + static Field explosion( ExplosionType t ); + + bool solid() const; + Destruction destructible() const; + void draw( int x, int y ) const; + + bool is_floor_family() const { return kind != Kind::wall; } +}; + +class Map +{ +public: + explicit Map( const std::string& fn ); + ~Map(); // defined in map.cpp where the entity types are complete + + Field& at( int x, int y ) { return grid[index( x, y )]; } + const Field& at( int x, int y ) const { return grid[index( x, y )]; } + + void draw(); + void tick( World& world ); + + int getx() const { return X; } + int gety() const { return Y; } + int get_crates() const { return destructibles; } + + std::unique_ptr create_player() const; + + void place_bomb( int x, int y ); + bool monster_collide( int tx, int ty ) const; + +private: + static constexpr int X = 13, Y = 11; + + // Deferred monster respawn timer, mirroring Map.MWait. + struct MWait + { + int type; // 1, 2 or 3 + std::int64_t time; // timestamp at which it respawns + }; + + static int index( int x, int y ) { return x * Y + y; } + + void load( const std::string& fn ); + void generate_destructibles(); + void populate_map(); + bool monster_ok( int rx, int ry, int px, int py, int r ) const; + + std::vector grid; + int px = -10, py = -10; + int destructibles = 0; + int m1 = 0, m2 = 0, m3 = 0; + + std::vector> bombs; + std::vector> monsters; + std::vector> bonuses; + std::vector mwait; +}; + +} diff --git a/examples/dyna/src/monster.cpp b/examples/dyna/src/monster.cpp new file mode 100644 index 00000000..111ba287 --- /dev/null +++ b/examples/dyna/src/monster.cpp @@ -0,0 +1,222 @@ +#include "monster.hpp" + +#include "gfx.hpp" +#include "map.hpp" +#include "texture.hpp" +#include "timer.hpp" +#include "world.hpp" + +namespace dyna +{ + +namespace +{ + +bool is_opposite( Action a, Action b ) +{ + return ( a == Action::up && b == Action::down ) || + ( a == Action::down && b == Action::up ) || + ( a == Action::left && b == Action::right ) || + ( a == Action::right && b == Action::left ); +} + +} // namespace + +Monster::Monster( int type, int gx, int gy ) + : mtype( type ) + , t( type == 1 ? 14 : type == 2 ? 11 + : 7 ) +{ + x = gx * 64; + y = gy * 64; +} + +void Monster::set_action( Action a ) +{ + Entity::set_action( a ); + if( action == Action::appear ) + left = 200; +} + +std::vector Monster::possible_dirs( const Map& map ) const +{ + std::vector dirs; + + if( x > 0 && !map.at( x / 64 - 1, y / 64 ).solid() ) + dirs.push_back( Action::left ); + if( x / 64 < map.getx() - 1 && !map.at( x / 64 + 1, y / 64 ).solid() ) + dirs.push_back( Action::right ); + if( y > 0 && !map.at( x / 64, y / 64 - 1 ).solid() ) + dirs.push_back( Action::up ); + if( y / 64 < map.gety() - 1 && !map.at( x / 64, y / 64 + 1 ).solid() ) + dirs.push_back( Action::down ); + + return dirs; +} + +bool Monster::straight( const std::vector& dirs ) +{ + return is_opposite( dirs[0], dirs[1] ); +} + +Action Monster::any_dir( const Map& map ) +{ + std::vector dirs = possible_dirs( map ); + if( dirs.empty() ) + return Action::wait; + return dirs[RNG::next( static_cast( dirs.size() ) )]; +} + +Action Monster::rand_dir( const Map& map ) +{ + Action tmp = any_dir( map ); + if( is_opposite( action, tmp ) ) + tmp = any_dir( map ); + return tmp; +} + +void Monster::think( const Map& map ) +{ + if( action == Action::wait || action == Action::appear ) + { + set_action( rand_dir( map ) ); + return; + } + + std::vector dirs = possible_dirs( map ); + + if( dirs.size() == 2 && straight( dirs ) ) + { + left = 64; + } + else + { + Action tmp = rand_dir( map ); + + if( tmp == action ) + { + left = 64; + } + else + { + set_action( tmp ); + if( tmp != Action::wait ) + left = 64; + } + } +} + +void Monster::tick( World& world ) +{ + Map& map = world.map(); + + delta += Timer::delta; + + while( delta > t ) + { + delta -= t; + + if( action == Action::wait ) + { + think( map ); + } + else if( left > 0 ) + { + left--; + + switch( action ) + { + case Action::down: y++; break; + case Action::up: y--; break; + case Action::left: x--; break; + case Action::right: x++; break; + default: break; + } + } + else + { + if( action == Action::death ) + die( world ); + else + think( map ); + } + + if( action != Action::death && killed( map ) ) + { + set_action( Action::death ); + left = 790 / t; + } + } +} + +void Monster::die( World& ) +{ + dead = true; +} + +const AnimTexture& Monster::texture_for( Action a ) const +{ + struct Set + { + const AnimTexture* wait; + const AnimTexture* up; + const AnimTexture* down; + const AnimTexture* left; + const AnimTexture* right; + const AnimTexture* death; + }; + + Set s; + if( mtype == 1 ) + s = { &Textures::m1_d, &Textures::m1_u, &Textures::m1_d, &Textures::m1_l, &Textures::m1_r, &Textures::m1_death }; + else if( mtype == 2 ) + s = { &Textures::m2_d, &Textures::m2_u, &Textures::m2_d, &Textures::m2_l, &Textures::m2_r, &Textures::m2_death }; + else + s = { &Textures::m3_d, &Textures::m3_u, &Textures::m3_d, &Textures::m3_l, &Textures::m3_r, &Textures::m3_death }; + + switch( a ) + { + case Action::up: return *s.up; + case Action::down: return *s.down; + case Action::left: return *s.left; + case Action::right: return *s.right; + case Action::death: return *s.death; + case Action::wait: + case Action::appear: + default: return *s.wait; // wait/appear use the "down" sprite + } +} + +void Monster::draw() +{ + // The original returns without drawing for unexpected actions; monsters only + // ever hold the actions handled by texture_for, so always draw. + generic_draw( texture_for( action ) ); +} + +void Monster::generic_draw( const AnimTexture& tex ) +{ + int frame; + + if( action == Action::wait ) + { + frame = 0; + } + else if( action == Action::appear ) + { + frame = 0; + Gfx::alpha( static_cast( 200 - left ) / 200.0f ); + } + else + { + frame = static_cast( ( Timer::get_timestamp() - action_start ) / 40 ); + } + + tex.bind( frame ); + Gfx::draw_sprite( x, y ); + + if( action == Action::appear ) + Gfx::alpha( 1.0f ); +} + +} diff --git a/examples/dyna/src/monster.hpp b/examples/dyna/src/monster.hpp new file mode 100644 index 00000000..0fba2679 --- /dev/null +++ b/examples/dyna/src/monster.hpp @@ -0,0 +1,41 @@ +#pragma once + +#include "entity.hpp" + +#include + +namespace dyna +{ + +class AnimTexture; + +// The three monster variants from monster.cs differed only in speed, sprite set +// and respawn delay, so they fold into one class parameterised by `type` (1-3). +class Monster : public Entity +{ +public: + Monster( int type, int gx, int gy ); + + void set_action( Action a ) override; + void tick( World& world ) override; + void draw() override; + void die( World& world ) override; + + bool is_dead() const { return dead; } + int type() const { return mtype; } + +private: + std::vector possible_dirs( const Map& map ) const; + static bool straight( const std::vector& dirs ); + Action rand_dir( const Map& map ); + Action any_dir( const Map& map ); // __rand_dir in the original + void think( const Map& map ); + void generic_draw( const AnimTexture& tex ); + const AnimTexture& texture_for( Action a ) const; + + int mtype; // 1, 2 or 3 + int t; // ms per movement sub-step (per-type speed) + bool dead = false; +}; + +} diff --git a/examples/dyna/src/player.cpp b/examples/dyna/src/player.cpp new file mode 100644 index 00000000..66335a4a --- /dev/null +++ b/examples/dyna/src/player.cpp @@ -0,0 +1,123 @@ +#include "player.hpp" + +#include "gfx.hpp" +#include "map.hpp" +#include "texture.hpp" +#include "timer.hpp" +#include "world.hpp" + +namespace dyna +{ + +Player::Player( int gx, int gy ) +{ + x = gx * 64; + y = gy * 64; + set_action( Action::wait ); + queue = Action::wait; +} + +void Player::tick( World& world ) +{ + Map& map = world.map(); + + delta += Timer::delta; + + while( delta > t ) + { + delta -= t; + + if( left > 0 ) + { + left--; + + switch( action ) + { + case Action::down: y++; break; + case Action::up: y--; break; + case Action::left: x--; break; + case Action::right: x++; break; + case Action::place_bomb: + if( left == 0 ) + map.place_bomb( x / 64, y / 64 ); + break; + default: + break; + } + } + else + { + if( action == Action::death ) + { + die( world ); + return; + } + if( map.at( x / 64, y / 64 ).kind == Field::Kind::vortex ) + { + world.next_level = true; + return; + } + if( !can_move( queue, map ) ) + queue = Action::wait; + + if( action != queue ) + set_action( queue ); + + if( action != Action::wait ) + left = 64; + if( action == Action::place_bomb ) + left = 32; + } + + if( action != Action::death && killed( map ) ) + { + set_action( Action::death ); + left = 1140 / t; + } + } +} + +void Player::draw() +{ + const AnimTexture* tex = nullptr; + + switch( action ) + { + case Action::wait: tex = &Textures::p_wait; break; + case Action::up: tex = &Textures::p_u; break; + case Action::down: tex = &Textures::p_d; break; + case Action::left: tex = &Textures::p_l; break; + case Action::right: tex = &Textures::p_r; break; + case Action::death: tex = &Textures::p_death; break; + case Action::place_bomb: tex = &Textures::p_wait; break; + default: + return; + } + + int frame = static_cast( Timer::get_timestamp() - action_start ); + frame /= ( action == Action::death ) ? 60 : 40; + tex->bind( frame ); + + Gfx::draw_sprite( x, y ); +} + +void Player::move( Action a ) +{ + queue = a; +} + +void Player::die( World& world ) +{ + world.killed = true; +} + +bool Player::killed( const Map& map ) const +{ + if( Entity::killed( map ) ) + return true; + if( map.monster_collide( x, y ) ) + return true; + return false; +} + +} diff --git a/examples/dyna/src/player.hpp b/examples/dyna/src/player.hpp new file mode 100644 index 00000000..ae1e8d45 --- /dev/null +++ b/examples/dyna/src/player.hpp @@ -0,0 +1,27 @@ +#pragma once + +#include "entity.hpp" + +namespace dyna +{ + +class Player : public Entity +{ +public: + Player( int gx, int gy ); + + void tick( World& world ) override; + void draw() override; + void die( World& world ) override; + + void move( Action a ); // queues the next direction; applied between tiles + +protected: + bool killed( const Map& map ) const override; + +private: + static constexpr int t = 6; // ms per movement sub-step + Action queue = Action::wait; +}; + +} diff --git a/examples/dyna/src/texture.cpp b/examples/dyna/src/texture.cpp new file mode 100644 index 00000000..884eba5d --- /dev/null +++ b/examples/dyna/src/texture.cpp @@ -0,0 +1,212 @@ +#include "texture.hpp" + +#include "datapath.hpp" +#include "gfx.hpp" + +#include +#include + +#include +#include +#include +#include +#include + +namespace dyna +{ + +void GlTexture::reset() +{ + if( id_ ) + { + // The texture globals outlive main(), so their destructors can run after + // the GL context is already gone (which frees its textures anyway). Only + // call into GL while a context is current; otherwise just drop the name. + if( SDL_GL_GetCurrentContext() ) + glDeleteTextures( 1, &id_ ); + id_ = 0; + } +} + +namespace +{ + +struct SurfaceDeleter +{ + void operator()( SDL_Surface* s ) const { SDL_DestroySurface( s ); } +}; +using SurfacePtr = std::unique_ptr; + +// Convert an arbitrary surface to tightly addressable RGBA8. Returns null on +// failure; the result owns its pixels. +SurfacePtr to_rgba( SDL_Surface* src ) +{ + if( !src ) return nullptr; + return SurfacePtr{ SDL_ConvertSurface( src, SDL_PIXELFORMAT_RGBA32 ) }; +} + +} // namespace + +bool Texture::load( const char* fn ) +{ + SurfacePtr image{ IMG_Load( fn ) }; + if( !image ) + { + std::fprintf( stderr, "Cannot open texture %s: %s\n", fn, SDL_GetError() ); + return false; + } + + SurfacePtr rgba = to_rgba( image.get() ); + if( !rgba ) + { + std::fprintf( stderr, "Cannot convert texture %s: %s\n", fn, SDL_GetError() ); + return false; + } + + // Pack the surface into a tight RGBA8 block, skipping any per-row padding. + const int w = rgba->w, h = rgba->h; + std::vector packed( static_cast( w ) * h * 4 ); + const auto* pixels = static_cast( rgba->pixels ); + for( int row = 0; row < h; row++ ) + { + std::memcpy( &packed[static_cast( row ) * w * 4], + pixels + static_cast( row ) * rgba->pitch, + static_cast( w ) * 4 ); + } + + tex_ = GlTexture{ Render::make_texture( w, h, 1, packed.data() ) }; + return static_cast( tex_ ); +} + +void Texture::bind() const +{ + Render::use_texture( tex_.get(), 0 ); +} + +void AnimTexture::load( SDL_Surface* sheet, int tilex, int tiley, int n ) +{ + SurfacePtr rgba = to_rgba( sheet ); + if( !rgba ) + { + std::fprintf( stderr, "Cannot convert sprite sheet: %s\n", SDL_GetError() ); + return; + } + + const auto* pixels = static_cast( rgba->pixels ); + const int pitch = rgba->pitch; + + // Lay the n frames out back to back as the layers of an array texture. + constexpr int frame_bytes = 64 * 64 * 4; + std::vector frames( static_cast( n ) * frame_bytes ); + for( int i = 0; i < n; i++ ) + { + for( int fy = 0; fy < 64; fy++ ) + { + int srcy = 64 * ( tiley + i ) + fy; + int srcx = 64 * tilex; + std::memcpy( &frames[static_cast( i ) * frame_bytes + static_cast( fy ) * 64 * 4], + pixels + static_cast( srcy ) * pitch + static_cast( srcx ) * 4, + static_cast( 64 ) * 4 ); + } + } + + tex_ = GlTexture{ Render::make_texture( 64, 64, n, frames.data() ) }; + frames_ = n; +} + +void AnimTexture::bind( int frame ) const +{ + if( frames_ <= 0 ) return; + int layer = frame % frames_; + if( layer < 0 ) layer += frames_; + Render::use_texture( tex_.get(), layer ); +} + +namespace Textures +{ +Texture menu, sand, wall, crate; + +AnimTexture p_wait, p_u, p_d, p_l, p_r, p_death; + +AnimTexture bomb, bomb_appear, e_c, e_h, e_v, e_le, e_re, e_de, e_ue; + +AnimTexture m1_death, m1_l, m1_r, m1_d, m1_u; +AnimTexture m2_death, m2_l, m2_r, m2_d, m2_u; +AnimTexture m3_death, m3_l, m3_r, m3_d, m3_u; + +AnimTexture bonus1, bonus2; + +AnimTexture vortex_appear, vortex; + +void preload() +{ + menu.load( data_path( "data/gfx/menu.png" ).c_str() ); + sand.load( data_path( "data/gfx/sand.png" ).c_str() ); + wall.load( data_path( "data/gfx/wall.png" ).c_str() ); + crate.load( data_path( "data/gfx/crate.png" ).c_str() ); + + { + SurfacePtr img{ IMG_Load( data_path( "data/gfx/Player.png" ).c_str() ) }; + p_wait.load( img.get(), 0, 0, 20 ); + p_d.load( img.get(), 1, 0, 20 ); + p_u.load( img.get(), 2, 0, 20 ); + p_l.load( img.get(), 3, 0, 20 ); + p_r.load( img.get(), 4, 0, 20 ); + p_death.load( img.get(), 5, 0, 20 ); + } + + { + SurfacePtr img{ IMG_Load( data_path( "data/gfx/Bomb.png" ).c_str() ) }; + bomb.load( img.get(), 0, 0, 10 ); + bomb_appear.load( img.get(), 5, 0, 10 ); + e_c.load( img.get(), 1, 0, 5 ); + e_h.load( img.get(), 2, 0, 5 ); + e_v.load( img.get(), 1, 5, 5 ); + e_le.load( img.get(), 3, 0, 5 ); + e_re.load( img.get(), 2, 5, 5 ); + e_de.load( img.get(), 4, 0, 5 ); + e_ue.load( img.get(), 3, 5, 5 ); + } + + { + SurfacePtr img{ IMG_Load( data_path( "data/gfx/monster1.png" ).c_str() ) }; + m1_death.load( img.get(), 0, 0, 20 ); + m1_u.load( img.get(), 1, 0, 10 ); + m1_l.load( img.get(), 2, 0, 10 ); + m1_d.load( img.get(), 1, 10, 10 ); + m1_r.load( img.get(), 2, 10, 10 ); + } + + { + SurfacePtr img{ IMG_Load( data_path( "data/gfx/monster2.png" ).c_str() ) }; + m2_death.load( img.get(), 0, 0, 20 ); + m2_d.load( img.get(), 1, 0, 20 ); + m2_u.load( img.get(), 2, 0, 20 ); + m2_l.load( img.get(), 3, 0, 20 ); + m2_r.load( img.get(), 4, 0, 20 ); + } + + { + SurfacePtr img{ IMG_Load( data_path( "data/gfx/monster3.png" ).c_str() ) }; + m3_death.load( img.get(), 0, 0, 20 ); + m3_d.load( img.get(), 1, 0, 9 ); + m3_u.load( img.get(), 2, 0, 9 ); + m3_l.load( img.get(), 1, 10, 9 ); + m3_r.load( img.get(), 2, 10, 9 ); + } + + { + SurfacePtr img{ IMG_Load( data_path( "data/gfx/bonusy.png" ).c_str() ) }; + bonus1.load( img.get(), 0, 0, 20 ); + bonus2.load( img.get(), 1, 0, 20 ); + } + + { + SurfacePtr img{ IMG_Load( data_path( "data/gfx/portal.png" ).c_str() ) }; + vortex_appear.load( img.get(), 0, 0, 20 ); + vortex.load( img.get(), 1, 0, 20 ); + } +} +} + +} diff --git a/examples/dyna/src/texture.hpp b/examples/dyna/src/texture.hpp new file mode 100644 index 00000000..e2144274 --- /dev/null +++ b/examples/dyna/src/texture.hpp @@ -0,0 +1,91 @@ +#pragma once + +#include + +struct SDL_Surface; + +namespace dyna +{ + +// Move-only RAII owner of a GL texture name. Every texture in the game is a +// GL_TEXTURE_2D_ARRAY (static images use a single layer, animations use one +// layer per frame) so the renderer only ever has to deal with one sampler type. +class GlTexture +{ +public: + GlTexture() = default; + explicit GlTexture( GLuint id ) noexcept : id_( id ) {} + ~GlTexture() { reset(); } + + GlTexture( GlTexture&& o ) noexcept : id_( o.id_ ) { o.id_ = 0; } + GlTexture& operator=( GlTexture&& o ) noexcept + { + if( this != &o ) + { + reset(); + id_ = o.id_; + o.id_ = 0; + } + return *this; + } + + GlTexture( const GlTexture& ) = delete; + GlTexture& operator=( const GlTexture& ) = delete; + + GLuint get() const { return id_; } + explicit operator bool() const { return id_ != 0; } + + void reset(); // glDeleteTextures; safe on an empty handle + +private: + GLuint id_ = 0; +}; + +// A single static texture loaded from a whole image file. Ported from +// texture.cs; binding just records the texture for the next draw call. +class Texture +{ +public: + bool load( const char* fn ); + void bind() const; + +private: + GlTexture tex_; +}; + +// A vertical strip of 64x64 animation frames cut out of a sprite sheet, stored +// as the layers of one array texture. Mirrors AnimTexture in texture.cs. +class AnimTexture +{ +public: + // Extract n frames from column `tilex`, starting at row `tiley`, where each + // coordinate is in 64px tile units. Mirrors AnimTexture.load in texture.cs. + void load( SDL_Surface* sheet, int tilex, int tiley, int n ); + void bind( int frame ) const; // frame is taken modulo the frame count + +private: + GlTexture tex_; + int frames_ = 0; +}; + +// All game textures, loaded once at startup. Mirrors the Textures class. +namespace Textures +{ +extern Texture menu, sand, wall, crate; + +extern AnimTexture p_wait, p_u, p_d, p_l, p_r, p_death; + +extern AnimTexture bomb, bomb_appear, e_c, e_h, e_v, e_le, e_re, e_de, e_ue; + +extern AnimTexture m1_death, m1_l, m1_r, m1_d, m1_u; +extern AnimTexture m2_death, m2_l, m2_r, m2_d, m2_u; +extern AnimTexture m3_death, m3_l, m3_r, m3_d, m3_u; + +extern AnimTexture bonus1, bonus2; + +extern AnimTexture vortex_appear, vortex; + +void preload(); +} + +} diff --git a/examples/dyna/src/timer.cpp b/examples/dyna/src/timer.cpp new file mode 100644 index 00000000..270535f2 --- /dev/null +++ b/examples/dyna/src/timer.cpp @@ -0,0 +1,51 @@ +#include "timer.hpp" + +#include + +#include + +namespace dyna +{ + +namespace Timer +{ +int delta = 0; +static std::int64_t timestamp = 0; + +void reset() +{ + delta = 0; + timestamp = static_cast( SDL_GetTicks() ); +} + +int tick() +{ + std::int64_t tmp = timestamp; + timestamp = static_cast( SDL_GetTicks() ); + delta = static_cast( timestamp - tmp ); + return delta; +} + +std::int64_t get_timestamp() +{ + return timestamp; +} +} + +namespace RNG +{ +static std::mt19937& engine() +{ + static std::mt19937 e{ std::random_device{}() }; + return e; +} + +int next( int n ) +{ + if( n <= 0 ) return 0; + std::uniform_int_distribution dist( 0, n - 1 ); + return dist( engine() ); +} +} + +} diff --git a/examples/dyna/src/timer.hpp b/examples/dyna/src/timer.hpp new file mode 100644 index 00000000..2a299934 --- /dev/null +++ b/examples/dyna/src/timer.hpp @@ -0,0 +1,25 @@ +#pragma once + +#include + +namespace dyna +{ + +// Frame timing, ported from timer.cs. Timestamps are milliseconds since +// Timer::reset(); kept 64-bit so the modulo arithmetic the animation code +// relies on never overflows during a session. +namespace Timer +{ +void reset(); +int tick(); // advances the clock, returns delta in ms +std::int64_t get_timestamp(); +extern int delta; // ms elapsed during the last tick() +} + +// Thin wrapper over a single global PRNG, mirroring the C# RNG helper. +namespace RNG +{ +int next( int n ); // uniform in [0, n) +} + +} diff --git a/examples/dyna/src/world.cpp b/examples/dyna/src/world.cpp new file mode 100644 index 00000000..e49f5c5e --- /dev/null +++ b/examples/dyna/src/world.cpp @@ -0,0 +1,39 @@ +#include "world.hpp" + +#include "map.hpp" +#include "player.hpp" + +namespace dyna +{ + +World::World( const std::string& level_fn, bool with_player ) + : map_( std::make_unique( level_fn ) ) +{ + if( with_player ) + { + player_ = map_->create_player(); + crates_left = map_->get_crates(); + } + else + { + crates_left = -1; // the menu never opens an exit portal + } +} + +World::~World() = default; + +void World::tick() +{ + map_->tick( *this ); + if( player_ ) + player_->tick( *this ); +} + +void World::draw() +{ + map_->draw(); + if( player_ ) + player_->draw(); +} + +} diff --git a/examples/dyna/src/world.hpp b/examples/dyna/src/world.hpp new file mode 100644 index 00000000..faacfa3e --- /dev/null +++ b/examples/dyna/src/world.hpp @@ -0,0 +1,44 @@ +#pragma once + +#include +#include + +namespace dyna +{ + +class Map; +class Player; + +// Owns the state for one running level: the map, the player (absent on the +// menu screen), and the flags the gameplay code used to reach through global +// variables. Passing a World& into the tick path replaces the old Game::p / +// Game::current_map / Game::killed globals, so there are no non-owning pointers +// to outlive the objects they point at. +class World +{ +public: + // Loads `level_fn`; spawns a player from the map's '@' marker when + // with_player is set (gameplay) and leaves it null otherwise (menu). + World( const std::string& level_fn, bool with_player ); + ~World(); + + World( const World& ) = delete; + World& operator=( const World& ) = delete; + + Map& map() { return *map_; } + const Map& map() const { return *map_; } + Player* player() { return player_.get(); } // null on the menu screen + + void tick(); + void draw(); + + bool killed = false; + bool next_level = false; + int crates_left = 0; + +private: + std::unique_ptr map_; + std::unique_ptr player_; +}; + +}