bazald's adapation of DXTank (written by by Jon Voigt) to zenilib.
Set up the Development Environment, download zenilib, and learn how to build everything if you haven't already.
zenilib/jni/application/bootstrap.cpp
.
Play_State
:class Play_State : public Gamestate_Base { Play_State(const Play_State &); Play_State operator=(const Play_State &); public: Play_State() { set_pausable(true); } private: void on_push() { get_Window().set_mouse_state(Window::MOUSE_HIDDEN); } void on_pop() { get_Controllers().reset_vibration_all(); } void on_cover() { get_Controllers().reset_vibration_all(); } void on_controller_button(const SDL_ControllerButtonEvent &event) { if(event.button == SDL_CONTROLLER_BUTTON_BACK && event.state == SDL_PRESSED) get_Game().push_Popup_Menu_State(); } };This will allow the game to be paused. When the game is not paused, the mouse will be hidden from view. Otherwise, defaults are inherited from Gamestate_Base.
Play_State
is referenced by a line in Bootstrap::Gamestate_One_Initializer::operator()()
.
zenilib Application
and Zenipex Library\nApplication
in Bootstrap::Gamestate_One_Initializer::operator()()
with the titles of your choosing. I recommend zenilib Tank Tutorial
and zenilib:\nTank Tutorial
respectively.
zenilib/assets/textures/
, creating the directory textures/
as needed.
zenilib/assets/config/textures.xml
.
<tank> <filepath>textures/tank.png</filepath> <tile>0</tile> </tank>and save
textures.xml
.textures.xml
, this tells zenilib to give you access
to zenilib/assets/textures/tank.png
via the alias tank
. Additionally, setting tile to 0
tells zenilib not to allow the texture to be tiled.
(If you don't know whether you will need tiling or not, it is safer to use 1
.)
class Play_State
, define class Game_Object
:Game_Object
in our 2D world clear needs a position and some sort of size.
Additionally, we will give our Game_Object
s a velocity so that we can
move them easily.class Game_Object { public: Game_Object(const Point2f &position_, const Vector2f &size_, const float &theta_ = 0.0f, const float &speed_ = 0.0f) : m_position(position_), m_size(size_), m_theta(theta_), m_speed(speed_) { } // If you might delete base class pointers, you need a virtual destructor. virtual ~Game_Object() {} private: Point2f m_position; // Upper left corner Vector2f m_size; // (width, height) float m_theta; float m_speed; };
Game_Object
, before Play_State
, define class Tank
:class Tank : public Game_Object { public: Tank(const Point2f &position_, const Vector2f &size_, const float &theta_) : Game_Object(position_, size_, theta_) { } };Instances of
Tank
can now be defined in Play_State
. But what can we do with them?
Tank
s on the screen. And let's do it in a way that makes it unlikely that we will have to duplicate work for other children of Game_Object
.Game_Object
, and then just add a little
bit of extra information in Tank
.// in class Game_Object {...} public: virtual void render() const = 0; // pure virtual function call protected: void render(const String &texture, const Color &filter = Color()) const { // Use a helper defined in Zeni/EZ2D.h render_image( texture, // which texture to use m_position, // upper-left corner m_position + m_size, // lower-right corner m_theta, // rotation in radians 1.0f, // scaling factor m_position + 0.5f * m_size, // point to rotate & scale about false, // whether or not to horizontally flip the texture filter); // what Color to "paint" the texture } // ... // in class Tank {...} public: void render() const { Game_Object::render("tank"); }
Tank
to our Play_State
and actually display it on the screen:class Play_State : public Gamestate_Base { Play_State(const Play_State &); Play_State operator=(const Play_State &); public: Play_State() : m_tank(Point2f(0.0f, 0.0f), Vector2f(64.0f, 64.0f), Global::pi * 1.5f) { set_pausable(true); } private: void on_push() { get_Window().set_mouse_state(Window::MOUSE_HIDDEN); } void render() { get_Video().set_2d(); m_tank.render(); } Tank m_tank; };
Gamestate
.class Play_State : public Gamestate_Base { Play_State(const Play_State &); Play_State operator=(const Play_State &); public: Play_State() : m_tank(Point2f(0.0f, 0.0f), Vector2f(64.0f, 64.0f), Global::pi * 1.5f), m_forward(false), m_turn_left(false), m_backward(false), m_turn_right(false) { set_pausable(true); } private: void on_push() { get_Window().set_mouse_state(Window::MOUSE_HIDDEN); } void on_key(const SDL_KeyboardEvent &event) { switch(event.keysym.sym) { case SDLK_w: m_forward = event.type == SDL_KEYDOWN; break; case SDLK_a: m_turn_left = event.type == SDL_KEYDOWN; break; case SDLK_s: m_backward = event.type == SDL_KEYDOWN; break; case SDLK_d: m_turn_right = event.type == SDL_KEYDOWN; break; default: Gamestate_Base::on_key(event); // Let Gamestate_Base handle it break; } } void render() { get_Video().set_2d(); m_tank.render(); } Tank m_tank; bool m_forward; bool m_turn_left; bool m_backward; bool m_turn_right; };
Tank
still isn't moving. We need to do something in perform_logic()
.Tank
by an amount depending on how much time has passed.// in class Game_Object {...} public: void turn_left(const float &theta_) { m_theta += theta_; } void move_forward(const float &move_) { // Performance consideration: calculate and store a forward Vector2f when turning and avoid cosine & sine here? m_position.x += move_ * cos(m_theta); m_position.y += move_ * -sin(m_theta); } // in the initializer list for Play_State::Play_State() , m_time_passed(0.0f) // in Play_State::on_push() {...} m_chrono.start(); // in Play_State::on_pop() {...} m_chrono.stop(); // in class Play_State {...} private: Chronometer<Time> m_chrono; float m_time_passed; void perform_logic() { const float time_passed = m_chrono.seconds(); const float time_step = time_passed - m_time_passed; m_time_passed = time_passed; // without a multiplier, this will rotate a full turn after ~6.28s m_tank.turn_left((m_turn_left - m_turn_right) * time_step); // without the '100.0f', it would move at ~1px/s m_tank.move_forward((m_forward - m_backward) * time_step * 100.0f); }
zenilib/assets/textures/
.
zenilib/assets/config/textures.xml
.
<bullet> <filepath>textures/bullet.png</filepath> <tile>0</tile> </bullet>and save
textures.xml
.Game_Object
and before class Tank
, define class Bullet
:class Bullet : public Game_Object { public: Bullet(const Point2f &position_, const Vector2f &size_, const float &theta_) : Game_Object(position_, size_, theta_) { } void render() const { Game_Object::render("bullet"); } };See how easy this was because of our base class, shared with
Tank
? It is still a bit verbose, but we basically 'find-replace-all'ed Tank
for Bullet
and tank
for bullet
.
// in class Game_Object {...} public: const Point2f & get_position() const {return m_position;} const Vector2f & get_size() const {return m_size;} const float & get_theta() const {return m_theta;} const float get_radius() const { return 0.5f * m_size.magnitude(); } // in class Tank {...} public: Bullet * fire() const { const float radius = 1.2f * get_radius(); const Vector2f bullet_size(8.0f, 8.0f); const Point2f position(get_position() + // the Tank's upper-left coordinate 0.5f * (get_size() - bullet_size) + // shift to center of the Tank radius * Vector2f(cos(get_theta()), -sin(get_theta()))); // then shift in front of the Tank return new Bullet(position, bullet_size, get_theta()); } // in the initializer list for Play_State::Play_State() , m_fire(false) // in the switch statement in Play_State::on_key(...) {...} case SDLK_SPACE: m_fire = event.type == SDL_KEYDOWN; break; // in Play_State::perform_logic() {...} if(m_fire) { m_fire = false; m_bullets.push_back(m_tank.fire()); } // in class Play_State {...} private: bool m_fire; list<Bullet *> m_bullets; public: ~Play_State() { for(list<Bullet *>::iterator it = m_bullets.begin(); it != m_bullets.end(); ++it) delete *it; }
// in Play_State::render() {...} for(list<Bullet *>::const_iterator it = m_bullets.begin(); it != m_bullets.end(); ++it) (*it)->render();
// in Play_State::perform_logic() {...} for(list<Bullet *>::iterator it = m_bullets.begin(); it != m_bullets.end(); ++it) (*it)->move_forward(time_step * 200.0f);
// in Play_State::perform_logic() {...} for(list<Bullet *>::iterator it = m_bullets.begin(); it != m_bullets.end();) { const Point2f &p = (*it)->get_position(); if(p.x < -10.0f || p.x > 10.0f + get_Window().get_width() || p.y < -10.0f || p.y > 10.0f + get_Window().get_height()) { delete *it; it = m_bullets.erase(it); } else ++it; }Note: this may be the wrong time to remove bullets in your game. Things happening off-screen can be important!
zenilib/assets/textures/
.
zenilib/assets/config/textures.xml
.
<boom> <filepath>textures/boom.png</filepath> <tile>0</tile> </boom>and save
textures.xml
.// in class Game_Object {...} public: bool collide(const Game_Object &rhs) const { const Vector2f dist_vec = m_position - rhs.m_position + 0.5f * (m_size - rhs.m_size); const float dist2 = dist_vec * dist_vec; const float radius_sum = get_radius() + rhs.get_radius(); return dist2 < radius_sum * radius_sum; } // in the initializer list for Tank::Tank(...) , m_exploded(false) // in class Tank {...} private: bool m_exploded; public: bool has_exploded() {return m_exploded;} void collide(const list<Bullet *> &bullets) { if(!m_exploded) for(list<Bullet *>::const_iterator it = bullets.begin(); it != bullets.end(); ++it) if((*it)->collide(*this)) { m_exploded = true; break; } } // in Tank::render() {...} if(m_exploded) render_image("boom", get_position(), get_position() + get_size()); // in Play_State::perform_logic() {...} m_tank.collide(m_bullets); // and change m_tank.*(...) to if(!m_tank.has_exploded()) { m_tank.turn_left((m_turn_left - m_turn_right) * time_step); m_tank.move_forward((m_forward - m_backward) * time_step * 100.0f); if(m_fire) { m_fire = false; m_bullets.push_back(m_tank.fire()); } }
// in class Play_State {...} private: Tank m_enemy; // in the initializer list for Play_State::Play_State() , m_enemy(Point2f(400.0f, 400.0f), Vector2f(64.0f, 64.0f), Global::pi * 0.75f) // in Play_State::perform_logic() {...} m_enemy.collide(m_bullets); // in Play_State::render() {...} m_enemy.render();
// in Play_State {...} private: bool m_enemy_forward; bool m_enemy_turn_left; bool m_enemy_backward; bool m_enemy_turn_right; bool m_enemy_fire; // in the initializer list for Play_State::Play_State() {...} , m_enemy_forward(false) , m_enemy_turn_left(false) , m_enemy_backward(false) , m_enemy_turn_right(false) , m_enemy_fire(false) // in the switch statement in Play_State::on_key(...) {...} case SDLK_UP: m_enemy_forward = event.type == SDL_KEYDOWN; break; case SDLK_LEFT: m_enemy_turn_left = event.type == SDL_KEYDOWN; break; case SDLK_DOWN: m_enemy_backward = event.type == SDL_KEYDOWN; break; case SDLK_RIGHT: m_enemy_turn_right = event.type == SDL_KEYDOWN; break; case SDLK_RETURN: m_enemy_fire = event.type == SDL_KEYDOWN; break; // in Play_State::perform_logic(...) {...} if(!m_enemy.has_exploded()) { m_enemy.turn_left((m_enemy_turn_left - m_enemy_turn_right) * time_step); m_enemy.move_forward((m_enemy_forward - m_enemy_backward) * time_step * 100.0f); if(m_enemy_fire) { m_enemy_fire = false; m_bullets.push_back(m_enemy.fire()); } }Note that it would probably be better to move the block just added in
perform_logic
into the class itself, as a generic step
function dependent on the time_step
value obtained in perform_logic
. That way, a bit of duplicate code could be removed from perform_logic
.
zenilib/assets/textures/
.
zenilib/assets/config/textures.xml
.
<logo> <filepath>textures/logo.png</filepath> <tile>0</tile> </logo>and save
textures.xml
.class Play_State
and class Gamestate_One
, define class Title_State_Custom
:class Title_State_Custom : public Title_State<Play_State, Instructions_State> { public: Title_State_Custom() : Title_State<Play_State, Instructions_State>("") { m_widgets.unlend_Widget(title); } void render() { Title_State<Play_State, Instructions_State>::render(); render_image("logo", Point2f(200.0f, 25.0f), Point2f(600.0f, 225.0f)); } };
Bootstrap::Gamestate_One_Initializer::operator()()
to be to Title_State_Custom
rather than Title_State<Play_State, Instructions_State>
.
// in class Play_State {...} private: Color m_prev_clear_color; // in the initializer list for Play_State::Play_State() {...} , m_prev_clear_color(get_Video().get_clear_Color()) // in Play_State::on_push() {...} get_Video().set_clear_Color(Color(1.0f, 0.26f, 0.13f, 0.0f)); // in Play_State::on_pop() {...} get_Video().set_clear_Color(m_prev_clear_color);
Title_State
after a set amount of time after an explosion:// in Play_State {...} private: Chronometer<Time> m_explosion; // in Play_State::perform_logic() {...} if(m_tank.has_exploded() || m_enemy.has_exploded()) if(m_explosion.is_running()) { if(m_explosion.seconds() > 3.0f) get_Game().pop_state(); } else m_explosion.start();
Make the Play_State
screen-resolution independent:
// at the very beginning of Play_State::render() {...} get_Video().set_2d(make_pair(Point2f(0.0f, 0.0f), Point2f(854.0f, 480.0f)), true); /** The following is optional because we aren't using the mouse in Play_State **/ // in Play_State {...} private: Projector2D m_projector; // in Play_State::perform_logic() m_projector = Projector2D(make_pair(Point2f(0.0f, 0.0f), Point2f(854.0f, 480.0f)), get_Video().get_viewport()); // finally, note that the Projector classes can do conversions between // screen space and your virtual screen size (e.g. 854x480) for you (and your Widgets)
Note that it might be nice to support different aspect ratios rather than strictly forcing a fixed width and height.
Make bullet rendering more efficient:
// TODO: Demo vr.apply_Texture(...)/vr.unapply_Texture(...) and manual quad rendering
It is important to note that Zeni::render_image(...)
is a relatively slow render path.
Bullets don't need much of the included functionality (rotation, color filters, ...) and resetting the Material
for each bullet has a performance penalty associated with it.