1
General discussions / Simplifying SFML
« on: March 06, 2014, 06:39:35 am »
I was looking for a portable C++ framework for some simple simulation of planets/bodies dancing around in space, which was meant for some writings. I tried SDL but it seemed too low level. SFML however seems all right so far (I just worked through some of the tutorials).
What I want to expose of code is just the logical model, with a minimum of support code.
So I factored out the support code so that a main program (working code), with antialised drawing and support for window resizing without stretching, looks like this, where xsf is my namespace for this:
But maybe others have done things in the same direction before, and there is already some C++ support framework for SFML that reduces code to something like the above?
Cheers,
- Alf
PS: This thing is still only at the stage of exploring the basics of SFML, and wrapping it in C++. I just think it's a good idea to get feedback before one has done too much! Both about directions, technicalities and alternatives.
PPS: The support code, complete as-of-this-posting (I have not yet even split it up in separate headers):
What I want to expose of code is just the logical model, with a minimum of support code.
So I factored out the support code so that a main program (working code), with antialised drawing and support for window resizing without stretching, looks like this, where xsf is my namespace for this:
#include <xsf/all.h> // xfs::*, fs::*
class Moving_ball
: public xsf::Model
{
using Circle = xsf::shape::Circle;
private:
Circle ball_{ Circle::Params()
.radius( 10.0 )
.fillcolor( sf::Color::Green )
};
xsf::Size<> velocity_{ 30*3.14159f, 30*2.71828f };
void on_update( sf::Time const time_now, sf::Time const dt ) override
{
XSF_INTENTIONALLY_UNUSED( time_now );
float const seconds = dt.asSeconds();
ball_.move( seconds*velocity_ );
sf::Vector2f const position = ball_.getPosition();
if( position.x < 0 && velocity_.x < 0 ) { velocity_.x *= -1; }
if( position.x > 100 && velocity_.x > 0 ) { velocity_.x *= -1; }
if( position.y < 0 && velocity_.y < 0 ) { velocity_.y *= -1; }
if( position.y > 100 && velocity_.y > 0 ) { velocity_.y *= -1; }
}
public:
Moving_ball()
{ drawables_.insert( &ball_ ); }
};
auto main()
-> int
{
Moving_ball model;
xsf::Window window( "SFML works, yay!", &model );
xsf::Controller( &model ).process_events();
}
class Moving_ball
: public xsf::Model
{
using Circle = xsf::shape::Circle;
private:
Circle ball_{ Circle::Params()
.radius( 10.0 )
.fillcolor( sf::Color::Green )
};
xsf::Size<> velocity_{ 30*3.14159f, 30*2.71828f };
void on_update( sf::Time const time_now, sf::Time const dt ) override
{
XSF_INTENTIONALLY_UNUSED( time_now );
float const seconds = dt.asSeconds();
ball_.move( seconds*velocity_ );
sf::Vector2f const position = ball_.getPosition();
if( position.x < 0 && velocity_.x < 0 ) { velocity_.x *= -1; }
if( position.x > 100 && velocity_.x > 0 ) { velocity_.x *= -1; }
if( position.y < 0 && velocity_.y < 0 ) { velocity_.y *= -1; }
if( position.y > 100 && velocity_.y > 0 ) { velocity_.y *= -1; }
}
public:
Moving_ball()
{ drawables_.insert( &ball_ ); }
};
auto main()
-> int
{
Moving_ball model;
xsf::Window window( "SFML works, yay!", &model );
xsf::Controller( &model ).process_events();
}
But maybe others have done things in the same direction before, and there is already some C++ support framework for SFML that reduces code to something like the above?
Cheers,
- Alf
PS: This thing is still only at the stage of exploring the basics of SFML, and wrapping it in C++. I just think it's a good idea to get feedback before one has done too much! Both about directions, technicalities and alternatives.
PPS: The support code, complete as-of-this-posting (I have not yet even split it up in separate headers):
#pragma once
// Copyright © 2014 Alf P. Steinbach.
#include <SFML/Graphics.hpp>
#include <algorithm> // std::count_if
#include <functional> // std::function
#include <set> // std::set
#define XSF_INTENTIONALLY_UNUSED( arg ) (void) arg; struct arg;
namespace xsf {
//---------------------------------------- Geometry:
template< class Number = float >
class Point
: public sf::Vector2< Number >
{
using Base = sf::Vector2< Number >;
public:
Point(): Base( 0, 0 ) {}
Point( Number const x, Number const y ): Base( x, y ) {}
};
template< class Number = float >
class Size
: public sf::Vector2< Number >
{
using Base = sf::Vector2< Number >;
public:
Size(): Base( 0, 0 ) {}
Size( Number const x, Number const y ): Base( x, y ) {}
};
inline
auto antialiased()
-> sf::ContextSettings
{
sf::ContextSettings settings;
settings.antialiasingLevel = 8;
return settings;
}
//---------------------------------------- Time:
inline
auto abs( sf::Time const& time )
-> sf::Time
{ return (time < sf::Time()? -time : time); }
inline
auto sign( sf::Time const& time )
-> int
{ return (time < sf::Time()? -1 : time == sf::Time()? 0 : +1); }
//---------------------------------------- Model:
class Model
{
friend class Controller;
private:
sf::Time max_dt_;
// dt, for "delta of time", is the time span since the previous update.
virtual
void on_update( sf::Time const time_now, sf::Time const dt )
= 0;
protected:
std::set< sf::Drawable const* > drawables_;
void update_to( sf::Time const time_now, sf::Time const dt )
{
sf::Time const abs_dt = abs( dt );
if( abs_dt <= max_dt_ )
{
on_update( time_now, dt );
}
else
{
bool const is_positive = (dt > sf::Time());
sf::Time const previous_time = time_now - dt;
sf::Time elapsed;
for( elapsed = max_dt_; elapsed < abs_dt; elapsed += max_dt_ )
{
if( is_positive )
{
on_update( previous_time + elapsed, max_dt_ );
}
else
{
on_update( previous_time - elapsed, -max_dt_ );
}
}
elapsed -= max_dt_;
if( elapsed < abs_dt )
{
sf::Time const remainder = abs_dt - elapsed;
on_update( time_now, (is_positive? remainder : -remainder) );
}
}
}
void set_max_dt( sf::Time const& new_max_dt )
{ max_dt_ = new_max_dt; }
public:
auto max_dt() const -> sf::Time { return max_dt_; }
auto drawables() const
-> std::set< sf::Drawable const* > const&
{ return drawables_; }
virtual ~Model() {}
Model()
: max_dt_( sf::seconds( 0.1f ) )
{}
};
namespace shape {
class Circle
: public sf::CircleShape
{
using Base = sf::CircleShape;
public:
class Params
{
friend class Circle;
private:
sf::Color fillcolor_ { sf::Color::White };
int n_points_ { 90 };
Point<float> position_ { 0, 0 };
float radius_ { 100 };
public:
using Outer_class = Circle;
auto fillcolor( sf::Color const& value )
-> Params&
{ fillcolor_ = value; return *this; }
auto n_points( int const value )
-> Params&
{ n_points_ = value; return *this; }
auto position( Point<float> const& value )
-> Params&
{ position_ = value; return *this; }
auto radius( float const value )
-> Params&
{ radius_ = value; return *this; }
};
void move_to( Point<float> const& position )
{ Base::setPosition( position ); }
Circle( float const radius, int const n_points = 90 )
: Base( radius, n_points )
{}
Circle( Params const& params )
: Base( params.radius_, params.n_points_ )
{
Base::setFillColor( params.fillcolor_ );
Base::setPosition( params.position_ );
}
};
} // namespace shape
//---------------------------------------- Rendering:
class Controller;
class Window
: public sf::RenderWindow
{
friend class Controller;
private:
Model const* p_model_;
// Called by controller's event processing loop.
static auto current_windows()
-> std::set<Window*>&
{
static std::set<Window*> the_windows;
return the_windows;
}
// Called by controller's event processing loop.
virtual void on( sf::Event const event )
{
switch( event.type )
{
case sf::Event::Closed:
{
close();
break;
}
case sf::Event::Resized:
{
// Avoid stretching of graphics to window size.
sf::FloatRect const visible_area(
0, 0, (float)event.size.width, (float)event.size.height
);
setView( sf::View( visible_area ) );
break;
}
}
}
virtual void render_model()
{
if( p_model_ != nullptr )
{
for( auto p_drawable : p_model_->drawables() )
{
draw( *p_drawable );
}
}
}
// Called by controller's event processing loop.
virtual void render()
{
clear();
render_model();
}
public:
~Window() override
{ current_windows().erase( this ); }
explicit Window(
char const* const title,
Model const* const p_model = nullptr,
Point<int> const& size = Point<int>( 320, 200 )
)
: sf::RenderWindow(
sf::VideoMode( size.x, size.y ),
title,
sf::Style::Default,
antialiased()
)
, p_model_( p_model )
{
sf::Window::setVerticalSyncEnabled( true );
current_windows().insert( this );
}
};
//---------------------------------------- Control:
class Controller
{
public:
using Event_processing_func = std::function< void( Window&, sf::Event const& ) >;
static
void default_event_processing( Window& window, sf::Event const& event )
{ window.on( event ); }
using Rendering_func = std::function< void( Window& ) >;
static
void default_rendering( Window& window )
{ window.render(); }
private:
sf::Clock clock_;
sf::Time previous_time_;
Model* p_model_;
void update()
{
sf::Time const current_time = clock_.getElapsedTime();
if( p_model_ != nullptr )
{
p_model_->update_to( current_time, current_time - previous_time_ );
}
previous_time_ = current_time;
}
void dispatch_events( Event_processing_func const process_event )
{
sf::Event event;
for( auto const p_window : Window::current_windows() )
{
while( p_window->pollEvent( event ) )
{
process_event( *p_window, event );
}
}
}
auto render_and_count_open_windows( Rendering_func const render )
-> int
{
int n = 0;
for( auto const p_window : Window::current_windows() )
{
if( p_window->isOpen() )
{
render( *p_window );
p_window->display();
++n;
}
}
return n;
}
public:
void process_events(
Rendering_func const render = default_rendering,
Event_processing_func const process_event = default_event_processing
)
{
for( ;; )
{
update();
dispatch_events( process_event );
int const n_open_windows = render_and_count_open_windows( render );
if( n_open_windows == 0 ) { break; }
}
}
Controller( Model* p_model = nullptr )
: p_model_( p_model )
{}
};
} // namespace xsf
// Copyright © 2014 Alf P. Steinbach.
#include <SFML/Graphics.hpp>
#include <algorithm> // std::count_if
#include <functional> // std::function
#include <set> // std::set
#define XSF_INTENTIONALLY_UNUSED( arg ) (void) arg; struct arg;
namespace xsf {
//---------------------------------------- Geometry:
template< class Number = float >
class Point
: public sf::Vector2< Number >
{
using Base = sf::Vector2< Number >;
public:
Point(): Base( 0, 0 ) {}
Point( Number const x, Number const y ): Base( x, y ) {}
};
template< class Number = float >
class Size
: public sf::Vector2< Number >
{
using Base = sf::Vector2< Number >;
public:
Size(): Base( 0, 0 ) {}
Size( Number const x, Number const y ): Base( x, y ) {}
};
inline
auto antialiased()
-> sf::ContextSettings
{
sf::ContextSettings settings;
settings.antialiasingLevel = 8;
return settings;
}
//---------------------------------------- Time:
inline
auto abs( sf::Time const& time )
-> sf::Time
{ return (time < sf::Time()? -time : time); }
inline
auto sign( sf::Time const& time )
-> int
{ return (time < sf::Time()? -1 : time == sf::Time()? 0 : +1); }
//---------------------------------------- Model:
class Model
{
friend class Controller;
private:
sf::Time max_dt_;
// dt, for "delta of time", is the time span since the previous update.
virtual
void on_update( sf::Time const time_now, sf::Time const dt )
= 0;
protected:
std::set< sf::Drawable const* > drawables_;
void update_to( sf::Time const time_now, sf::Time const dt )
{
sf::Time const abs_dt = abs( dt );
if( abs_dt <= max_dt_ )
{
on_update( time_now, dt );
}
else
{
bool const is_positive = (dt > sf::Time());
sf::Time const previous_time = time_now - dt;
sf::Time elapsed;
for( elapsed = max_dt_; elapsed < abs_dt; elapsed += max_dt_ )
{
if( is_positive )
{
on_update( previous_time + elapsed, max_dt_ );
}
else
{
on_update( previous_time - elapsed, -max_dt_ );
}
}
elapsed -= max_dt_;
if( elapsed < abs_dt )
{
sf::Time const remainder = abs_dt - elapsed;
on_update( time_now, (is_positive? remainder : -remainder) );
}
}
}
void set_max_dt( sf::Time const& new_max_dt )
{ max_dt_ = new_max_dt; }
public:
auto max_dt() const -> sf::Time { return max_dt_; }
auto drawables() const
-> std::set< sf::Drawable const* > const&
{ return drawables_; }
virtual ~Model() {}
Model()
: max_dt_( sf::seconds( 0.1f ) )
{}
};
namespace shape {
class Circle
: public sf::CircleShape
{
using Base = sf::CircleShape;
public:
class Params
{
friend class Circle;
private:
sf::Color fillcolor_ { sf::Color::White };
int n_points_ { 90 };
Point<float> position_ { 0, 0 };
float radius_ { 100 };
public:
using Outer_class = Circle;
auto fillcolor( sf::Color const& value )
-> Params&
{ fillcolor_ = value; return *this; }
auto n_points( int const value )
-> Params&
{ n_points_ = value; return *this; }
auto position( Point<float> const& value )
-> Params&
{ position_ = value; return *this; }
auto radius( float const value )
-> Params&
{ radius_ = value; return *this; }
};
void move_to( Point<float> const& position )
{ Base::setPosition( position ); }
Circle( float const radius, int const n_points = 90 )
: Base( radius, n_points )
{}
Circle( Params const& params )
: Base( params.radius_, params.n_points_ )
{
Base::setFillColor( params.fillcolor_ );
Base::setPosition( params.position_ );
}
};
} // namespace shape
//---------------------------------------- Rendering:
class Controller;
class Window
: public sf::RenderWindow
{
friend class Controller;
private:
Model const* p_model_;
// Called by controller's event processing loop.
static auto current_windows()
-> std::set<Window*>&
{
static std::set<Window*> the_windows;
return the_windows;
}
// Called by controller's event processing loop.
virtual void on( sf::Event const event )
{
switch( event.type )
{
case sf::Event::Closed:
{
close();
break;
}
case sf::Event::Resized:
{
// Avoid stretching of graphics to window size.
sf::FloatRect const visible_area(
0, 0, (float)event.size.width, (float)event.size.height
);
setView( sf::View( visible_area ) );
break;
}
}
}
virtual void render_model()
{
if( p_model_ != nullptr )
{
for( auto p_drawable : p_model_->drawables() )
{
draw( *p_drawable );
}
}
}
// Called by controller's event processing loop.
virtual void render()
{
clear();
render_model();
}
public:
~Window() override
{ current_windows().erase( this ); }
explicit Window(
char const* const title,
Model const* const p_model = nullptr,
Point<int> const& size = Point<int>( 320, 200 )
)
: sf::RenderWindow(
sf::VideoMode( size.x, size.y ),
title,
sf::Style::Default,
antialiased()
)
, p_model_( p_model )
{
sf::Window::setVerticalSyncEnabled( true );
current_windows().insert( this );
}
};
//---------------------------------------- Control:
class Controller
{
public:
using Event_processing_func = std::function< void( Window&, sf::Event const& ) >;
static
void default_event_processing( Window& window, sf::Event const& event )
{ window.on( event ); }
using Rendering_func = std::function< void( Window& ) >;
static
void default_rendering( Window& window )
{ window.render(); }
private:
sf::Clock clock_;
sf::Time previous_time_;
Model* p_model_;
void update()
{
sf::Time const current_time = clock_.getElapsedTime();
if( p_model_ != nullptr )
{
p_model_->update_to( current_time, current_time - previous_time_ );
}
previous_time_ = current_time;
}
void dispatch_events( Event_processing_func const process_event )
{
sf::Event event;
for( auto const p_window : Window::current_windows() )
{
while( p_window->pollEvent( event ) )
{
process_event( *p_window, event );
}
}
}
auto render_and_count_open_windows( Rendering_func const render )
-> int
{
int n = 0;
for( auto const p_window : Window::current_windows() )
{
if( p_window->isOpen() )
{
render( *p_window );
p_window->display();
++n;
}
}
return n;
}
public:
void process_events(
Rendering_func const render = default_rendering,
Event_processing_func const process_event = default_event_processing
)
{
for( ;; )
{
update();
dispatch_events( process_event );
int const n_open_windows = render_and_count_open_windows( render );
if( n_open_windows == 0 ) { break; }
}
}
Controller( Model* p_model = nullptr )
: p_model_( p_model )
{}
};
} // namespace xsf