Zenipex

ZeniTank

Bazald's adapation of DXTank (written by by Jon Voigt) to Zenilib.


Index

  1. Setup the Development Environment
  2. Follow the Start Guide
  3. Add a Gamestate
  4. Add a Tank
  5. Add Bullets
  6. Add Explosions
  7. Add a Title Screen
  8. Polish

Instructions

Setup

Set up the Development Environment if you haven't already.

Start Guide

Follow the rest of the generic Start Guide.

Add a Gamestate

  1. In Visual Studio, open 'Application/Source Files/Gamestate_One.cpp'.
  2. Replace the included Play_State (between the line defining the vector g_args and the line opening namespace Zeni) with the following:
    class Play_State : public Gamestate_Base {
    public:
      
    private:
      
    };
    
    This will blindly accept the defaults given in Gamestate_Base.

    Note that 'Play_State' is referenced by a line in Gamestate_One::perform_logic().
  3. Optionally, replace "Zenipex Library Application" in Gamestate_One::Gamestate_One(...) and "Long Title:\nSubtitle" in Gamestate_One::perform_logic() with the titles of your choosing. I recommend "Zenilib Tank Tutorial" and "Zenilib:\nTank Tutorial" respectively.

Add a Tank

  1. Download (right-click & save-as) Tank facing East to 'zenilib/textures/', creating the directory 'textures/' as needed.
  2. Open 'zenilib/config/textures.xml'.
  3. Add an entry

      <tank>
        <filepath>textures/tank.png</filepath>
        <tile>0</tile>
      </tank>

    and save 'textures.xml'.

    As described in 'zenilib/config/HOWTO.txt', this tells Zenilib to give you access to '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.)
  4. Before the definition of 'class Play_State', define a Game_Object:

    A 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 Vector3f &size_,
                  const float &theta_ = 0.0f,
                  const float &speed_ = 0.0f)
      : m_position(position_),
        m_size(size_),
        m_theta(theta_),
        m_speed(speed_)
      {
      }
    
    private:
      Point2f m_position; // Upper left corner
      Vector3f m_size; // (width, height, 0.0f)
      float m_theta;
    
      float m_speed;
    };
    
    Note that 'Vector3f's, 3D vectors, are used to represent 2D size and velocity. This is okay. So long as the third value is always ignored, they can be treated like 2D vectors. Non-3D vectors have such limited functionality and usefulness that they are not provided.
  5. After Game_Object, before Play_State, define class Tank:

    Our Tank is a simple extension of Game_Object.
    class Tank : public Game_Object {
    public:
      Tank(const Point2f &position_,
           const Vector3f &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?
  6. Let's make it possible to render Tanks 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.

    We're going to put most of the effort into 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
          Point2f(m_position.x + m_size.i, m_position.y + m_size.j), // lower-right corner
          m_theta, // rotation in radians
          1.0f, // scaling factor
          Point2f(m_position.x + 0.5f * m_size.i, m_position.y + 0.5f * m_size.j), // 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");
      }
    
  7. Now let's add a Tank to our Play_State and actually display it on the screen:
    class Play_State : public Gamestate_Base {
    public:
      Play_State()
        : m_tank(Point2f(0.0f, 0.0f), Vector3f(64.0f, 64.0f, 0.0f), pi * 1.5f)
      {
      }
    
    private:
      void render() {
        m_tank.render();
      }
    
      Tank m_tank;
    };
    
  8. Now let's add some keyboard controls:

    To do that, we need to override one of the input handling callback functions in our Gamestate.
    class Play_State : public Gamestate_Base {
    public:
      Play_State()
        : m_tank(Point2f(0.0f, 0.0f), Vector3f(64.0f, 64.0f, 0.0f), pi * 1.5f),
        m_forward(false),
        m_turn_left(false),
        m_backward(false),
        m_turn_right(false)
      {
      }
    
    private:
      void on_key(const SDL_KeyboardEvent &event) {
        switch(event.keysym.sym) {
          case SDLK_ESCAPE:
            get_Game().pop_state();
            break;
    
          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:
            break;
        }
      }
    
      void render() {
        m_tank.render();
      }
    
      Tank m_tank;
      bool m_forward;
      bool m_turn_left;
      bool m_backward;
      bool m_turn_right;
    };
    
  9. You may have noticed that the Tank still isn't moving. We need to do something in perform_logic().

    What we're going to do is move the 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_) {
        m_position.x += move_ * cos(m_theta);
        m_position.y += move_ * -sin(m_theta);
      }
    
    // ...
    
    // in class Play_State {...}
    private:
      Time m_time_passed;
      void perform_logic() {
        const Time m_current_time;
        const float time_step = m_current_time.get_seconds_since(m_time_passed);
        m_time_passed = m_current_time;
    
        // 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);
      }
    

Add Bullets

  1. Download (right-click & save-as)    --> Cannonball <--    to 'zenilib/textures/'.
  2. Open 'zenilib/config/textures.xml'.
  3. Add an entry

      <bullet>
        <filepath>textures/bullet.png</filepath>
        <tile>0</tile>
      </bullet>

    and save 'textures.xml'.
  4. After class 'Game_Object' and before class 'Tank', define class 'Bullet':
    class Bullet : public Game_Object {
    public:
      Bullet(const Point2f &position_,
             const Vector3f &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'.
  5. Now, to fire bullets, a bit of work is required:
    // in class Game_Object {...}
    public:
      const Point2f & get_position() const {return m_position;}
      const Vector3f & 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 Vector3f bullet_size(8.0f, 8.0f, 0.0f);
    
        const Point2f position(get_position().x +
                               0.5f * (get_size().i - bullet_size.i) +
                               radius * cos(get_theta()),
                               get_position().y +
                               0.5f * (get_size().j - bullet_size.j) +
                               radius * -sin(get_theta()));
    
        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;
      }
    
  6. And to render them, even more work is required:
    // in Play_State::render() {...}
        for(list<Bullet *>::const_iterator it = m_bullets.begin(); it != m_bullets.end(); ++it)
          (*it)->render();
    
  7. And to finally move them, still more work is required:
    // 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);
    
  8. One last step: We have to get rid of bullets that go off screen to keep good performance.
    // 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_Video().get_screen_width() ||
             p.y < -10.0f || p.y > 10.0f + get_Video().get_screen_height())
          {
            delete *it;
            it = m_bullets.erase(it);
          }
          else
            ++it;
        }
    

Add Explosions

  1. Download (right-click & save-as) Explosion with text: BOOM to 'zenilib/textures/'.
  2. Open 'zenilib/config/textures.xml'.
  3. Add an entry

      <boom>
        <filepath>textures/boom.png</filepath>
        <tile>0</tile>
      </boom>

    and save 'textures.xml'.
  4. // in class Game_Object {...}
    public:
      bool collide(const Game_Object &rhs) const {
        const float dist_x = m_position.x - rhs.m_position.x
                           + 0.5f * (m_size.i - rhs.m_size.i);
        const float dist_y = m_position.y - rhs.m_position.y
                           + 0.5f * (m_size.j - rhs.m_size.j);
        const float dist = sqrt(dist_x * dist_x + dist_y * dist_y);
    
        return dist < get_radius() + rhs.get_radius();
      }
    
    // 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(),
                       Point2f(get_position().x + get_size().i,
                               get_position().y + get_size().j));
    
    // 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());
          }
        }
    
  5. Now add a Tank for you to destroy:
    // in class Play_State {...}
    private:
      Tank m_enemy;
    
    // in the initializer list for Play_State::Play_State()
        , m_enemy(Point2f(400.0f, 400.0f), Vector3f(64.0f, 64.0f, 0.0f), pi * 0.75f)
    
    // in Play_State::perform_logic() {...}
        m_enemy.collide(m_bullets);
    
    // in Play_State::render() {...}
        m_enemy.render();
    
  6. Now add controls for the enemy.
    // 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.

Customize the Title Screen

  1. Download (right-click & save-as) Text: ZENITANK to 'zenilib/textures/'.
  2. Open 'zenilib/config/textures.xml'.
  3. Add an entry

      <logo>
        <filepath>textures/logo.png</filepath>
        <tile>0</tile>
      </logo>

    and save 'textures.xml'.
  4. Between class Play_State and class Gamestate_One, define a class Title_State_Custom:
    class Title_State_Custom : public Title_State<Play_State> {
    public:
      Title_State_Custom()
        : Title_State<Play_State>("")
      {
      }
    
      void render() {
        Title_State<Play_State>::render();
    
        render_image("logo", Point2f(200.0f, 25.0f), Point2f(600.0f, 225.0f));
      }
    };
    
  5. Change the transition in Gamestate_One::perform_logic() to be to Title_State_Custom rather than Title_State<Play_State>.

Polish

  1. Change the background color to brown for the duration of the game:
    // 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::Play_State() {...}
        get_Video().set_clear_color(Color(1.0f, 0.26f, 0.13f, 0.0f));
    
    // in Play_State::~Play_State() {...}
        get_Video().set_clear_color(m_prev_clear_color);
    
  2. Return to the 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.running()) {
            if(m_explosion.seconds() > 3.0f)
              get_Game().pop_state();
          }
          else
            m_explosion.start();
    
  3. 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(800.0f, 600.0f)));
    
    /** The following is optional because we aren't using the mouse in Play_State **/
    
    // in Play_State {...}
    private:
      Projector2D m_projector;
    
    // in the initalizer list of Play_State::Play_State()
        , m_projector(make_pair(Point2f(0.0f, 0.0f), Point2f(800.0f, 600.0f)))
    
    // finally, note that the Projector classes can do conversions between 
    // screen space and your virtual screen size (e.g. 800x600) for you (and your Widgets)