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().mouse_grab(true);
get_Window().mouse_hide(true);
//get_Game().joy_mouse.enabled = false;
}
void on_pop() {
//get_Window().mouse_grab(false);
get_Window().mouse_hide(false);
//get_Game().joy_mouse.enabled = true;
}
};
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.
to 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_Objects 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?
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.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().mouse_grab(true);
get_Window().mouse_hide(true);
//get_Game().joy_mouse.enabled = false;
}
void on_pop() {
//get_Window().mouse_grab(false);
get_Window().mouse_hide(false);
//get_Game().joy_mouse.enabled = true;
}
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().mouse_grab(true);
get_Window().mouse_hide(true);
//get_Game().joy_mouse.enabled = false;
}
void on_pop() {
//get_Window().mouse_grab(false);
get_Window().mouse_hide(false);
//get_Game().joy_mouse.enabled = true;
}
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_) {
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::Play_State() {...}
m_chrono.start();
// 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);
}
<--
to 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;
}
to 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 dist = sqrt(dist_vec * dist_vec);
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(),
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.
to 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(800.0f, 600.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(800.0f, 600.0f)), get_Video().get_viewport());
// 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)
Note that it might be nice to support different aspect ratios rather than strictly forcing a fixed width and height.