NOGDUS
November 14, 2018, 09:08:56 AM *
Welcome, Guest. Please login or register.

Login with username, password and session length
 
   Home   Blogs Help Search Tags Login Register  
Pages: [1]   Go Down
  Print  
Author Topic: Block Pushing - MVC  (Read 5026 times)
0 Members and 1 Guest are viewing this topic.
Richard Marks
202192397
Administrator
Member
*

Respect: 3425
Offline Offline

Posts: 1027


happy


« on: November 02, 2009, 10:11:45 PM »

Block Pushing - MVC

I was bored last night and started writing a block pushing game (aka Sokoban) and I'm going to share with you now the crazy code that I wrote over the period of 2 hours.

I started the project with a mindset of trying to develop using the MVC design in order to evaluate the idea of using it for game design.
My conclusion? I hate it. Its not me, and I'll not use it again.

I'm using C with some C++, and Allegro version 4.2.
My code editor is Code::Blocks, and I build things with Scons.

Okay, so now the first thing I did was I started with the good ole Allegro 6.2 template of mine and added some ground work (which I'm considering making a new template ...6.3 perhaps?  Grin

I'll just show the additional code and tell you how to integrate it into the 6.2 template instead of showing the whole template code over.

Code:
struct Demo
{
Demo()
{
}

~Demo()
{
}

bool update()
{
return true;
}

void render(BITMAP* target)
{
}
};

A simple starting point.
Let's integrate this with the template now.
We need a global pointer to this struct
Code:
Demo* demoapp = 0;


I'll finish this later, I'm being called to go work on something else... Roll Eyes
Logged

Richard Marks
202192397
Administrator
Member
*

Respect: 3425
Offline Offline

Posts: 1027


happy


« Reply #1 on: November 02, 2009, 11:44:57 PM »

Okay, sorry for that delay...where was I? ..Oh right, integrating the Demo structure with the template.

OK, locate the setup_game() function and add this code:
Code:
demoapp = new Demo;

Now the update_game() function needs this code:
Code:
if (!demoapp->update())
{
mainthreadisrunning = false;
}

And the render_game() function should do this too:
Code:
demoapp->render(backbuffer);

Finally, we cleanup in the shutdown_game() function with:
Code:
delete demoapp;

OK, so now we have our Demo structure integrated. (That was easy wasn't it?)

Next up, I added another structure, which I will show you shortly.
Stay tuned.
Logged

Richard Marks
202192397
Administrator
Member
*

Respect: 3425
Offline Offline

Posts: 1027


happy


« Reply #2 on: November 03, 2009, 12:09:42 AM »

I literally have no idea why I designed this next piece of code, but I did, and it works, so whatever. Roll Eyes

Code:
struct Grid
{
int rows_;
int columns_;
int width_;
int height_;
int outercolor_;
int innercolor_;
int fillcolor_;

BITMAP* cell_;

Grid(int rows, int columns, int width, int height, int outercolor, int innercolor, int fillcolor = 0) :
rows_(rows),
columns_(columns),
width_(width),
height_(height),
outercolor_(outercolor),
innercolor_(innercolor),
fillcolor_(fillcolor)
{
cell_ = create_bitmap(width / columns, height / rows);

clear_to_color(cell_, fillcolor_);
rect(cell_, 0, 0, cell_->w, cell_->h, outercolor_);
rect(cell_, 1, 1, cell_->w - 1, cell_->h - 1, innercolor_);
}

~Grid()
{
destroy_bitmap(cell_);
}

void render(BITMAP* target, int anchorx = 0, int anchory = 0)
{
for (int row = 0; row < rows_; row++)
{
for (int column = 0; column < columns_; column++)
{
blit(cell_, target, 0, 0, anchorx + (cell_->w * column), anchory + (cell_->h * row), cell_->w, cell_->h);
}
}
rect(target, anchorx, anchory, anchorx + (cell_->w * columns_), anchory + (cell_->h * rows_), outercolor_);
}

int get_cell_width() const { return cell_->w; }
int get_cell_height() const { return cell_->h; }
};

It merely provides a simple interface for adding a visible grid to your game.

Lets add it to our demo so we can see it in action.

Add a member variable pointer to Grid to the Demo struct:
Code:
Grid* grid_;

Now in the Demo Demo() constructor method, we create our instance:
Code:
const int BOARD_ROWS = 15;
const int BOARD_COLUMNS = 15;
const int BOARD_INNER_COLOR = makecol(0, 0, 128);
const int BOARD_OUTER_COLOR = makecol(96, 96, 96);
const int BOARD_FILL_COLOR = makecol(0, 0, 64);

grid_ = new Grid(
BOARD_ROWS, BOARD_COLUMNS,
480, 480,
BOARD_OUTER_COLOR, BOARD_INNER_COLOR, BOARD_FILL_COLOR);

This gives us a grid of fifteen by fifteen 32x32 pixel blocks that are blue and white.

In the Demo ~Demo() destructor method, we cleanup:
Code:
delete grid_;

Now of course we will not see the grid unless we render it, so in the Demo render() method:
Code:
grid_->render(target);

If you compile the program at this point, you should see the grid in the upper-left corner of the screen.
More to come, stay tuned.
Logged

Richard Marks
202192397
Administrator
Member
*

Respect: 3425
Offline Offline

Posts: 1027


happy


« Reply #3 on: November 03, 2009, 12:29:31 AM »

So now we get to the MVC stuff...yuck... but anyway, I told you already that I didn't like it.

I chose to start writing the Model for the World, and as common sense would go, I named the structure WorldModel.

Code:
struct WorldModel
{
WorldModel()
{
}

~WorldModel()
{
}
};

I thought for a moment what the Model would need, and its really just a typical tile map, so the WorldModel structure member variables are as follows:
Code:
int width_;
int height_;
int count_;
int* data_;

We store the width, and the height of the tile map (in tiles) and the total number of cells in the variable count_ and lastly we store our tile map data in a dynamically allocated 1D array.

I decided that that the model's constructor should create the data_ array, so I added some parameters to the WorldModel constructor:
Code:
WorldModel(int width, int height) :
width_(width),
height_(height),
count_(0),
data_(0)
{
}

And I added the code to the WorldModel() constructor to allocate the data array:
Code:
count_ = width_ * height_;
data_ = new int [count_];

Note that I pre-calculate the size of the array before I allocate the data.
I next figured that the Model should initialize the data array to zero so that we don't have to do that later.
I chose to add a method named clear() to the WorldModel structure.
Code:
void clear(int value = 0)
{
for (int index = 0; index < count_; index++)
{
data_[index] = value;
}
}
I added the option to be able to clear the tile map to any value as a convenience.

And then I call the new method from the WorldModel() constructor after we allocate the data array:
Code:
this->clear();

Now, we're allocating the array, and we need to de-allocate the array to cleanup.
So, in the ~WorldModel() destructor we simply delete the array:
Code:
delete [] data_;

I added the following methods to the WorldModel structure in order to make working with the data easier:
Code:
void set(int x, int y, int value)
{
int index = x + (y * width_);
if (index >= count_) { return; }
data_[index] = value;
}

int& operator[](int index)
{
return data_[index];
}

int& operator()(int x, int y)
{
int index = x + (y * width_);
if (index >= count_)
{
return data_[0];
}
return data_[index];
}

int get_width() const { return width_; }
int get_height() const { return height_; }
int get_count() const { return count_; }

I shouldn't need to explain them, they are rather self-explanatory.

I also decided to add some methods for saving and loading the tile map data.
I chose to have the user be responsible for opening and closing the file, and pass a FILE pointer to the methods.
Code:
void save(FILE* fp = 0)
{
if (!fp)
{
fprintf(stderr, "Error: WorldModel::save() called with invalid FILE pointer!\n");
return;
}

for (int index = 0; index < count_; index++)
{
int value = data_[index];
fwrite(&value, sizeof(int), 1, fp);
}
}

void load(FILE* fp = 0)
{
if (!fp)
{
fprintf(stderr, "Error: WorldModel::load() called with invalid FILE pointer!\n");
return;
}

for (int index = 0; index < count_; index++)
{
int value;
fread(&value, sizeof(int), 1, fp);
data_[index] = value;
}
}

And that wraps up the World Model structure. Cool
Next I will go over how we will use this model, and we will look at the Model View structure.
Until next time!
Logged

Richard Marks
202192397
Administrator
Member
*

Respect: 3425
Offline Offline

Posts: 1027


happy


« Reply #4 on: November 03, 2009, 01:25:38 AM »

So we have our Model for the World, and now we need to have a way to display it... this is called the View.
Keeping with the convention, I've named the structure WorldView.
Code:
struct WorldView
{
};

Before we can add any functionality, we need to think about what will need to be implemented.
I wanted to use a single image for the graphics, and I wanted to share the graphics with multiple structures.
So I decided that the host program (the Demo struct) will be responsible for managing the graphics resource and I will give a pointer to the graphics to each of my structures that require it.

For size information, I will ask the grid, so we need a pointer to the Grid struct as well.
And, we need a pointer to the model that is going to be displayed, so a pointer to the WorldModel struct is also needed.

So, lets add the member variables to our WorldView struct.
Code:
WorldModel* model_;
Grid* grid_;
BITMAP* graphics_;

Now we need the constructor to pass the values to init all our member variables with.
Everything is pointers because we are sharing the instances with multiple structures.
The destructor does not need to do anything now, because everything is shared.
We do NOT want to delete the instances in this struct.
Code:
WorldView(WorldModel* model, Grid* grid, BITMAP* graphics) :
model_(model),
grid_(grid),
graphics_(graphics)
{
}

~WorldView()
{
}

Our View structure only needs to have a single method for now, the render() method.
The purpose is clear, but the implementation may not be.
The graphics file is laid out in a simple horizontal format, with the tiles for the map first and the player last.

To draw a tile, we simply select to grab the graphics from a specific location.
We grab from the X,Y coordinate based on the tile value.
We multiply the value of the tile by the width of a single tile (the cellwidth) to get the X coordinate, and the Y coordinate is always zero.

To determine the location to draw the tile on the target bitmap, we simply multiply the position in the array by the size of the tile and add the anchor.

"Wait? Anchor?" You say.
Yeah, I chose to allow an easy method of drawing things in any position on the screen.
So every rendering method takes an optional X and Y coordinate set called the Anchor.
You may have noticed this in the Grid::render() method..but I forgot to explain it before. Cheesy

So, lets see the WorldView::render() method already:
Code:
void render(BITMAP* target, int anchorx = 0, int anchory = 0)
{
int width = model_->get_width();
int height = model_->get_height();
int cellwidth = grid_->get_cell_width();
int cellheight = grid_->get_cell_height();
for (int row = 0; row < height; row++)
{
int y = row * cellheight;
for (int column = 0; column < width; column++)
{
int x = column * cellwidth;
int value = (*model_)(column, row);
if (value)
{
blit(graphics_, target,
value * cellwidth, 0,
anchorx + x,
anchory + y, cellwidth, cellheight);
}
}
}
}

I will add some code to the Demo struct so that we can see this working in the next update.
Until then...




Logged

Richard Marks
202192397
Administrator
Member
*

Respect: 3425
Offline Offline

Posts: 1027


happy


« Reply #5 on: November 03, 2009, 01:56:08 AM »

So now it is time to integrate the World with the rest of our Demo.

We add some member variables to the Demo struct:
Code:
WorldModel* worldmodel_;
WorldView* worldview_;
BITMAP* graphics_;

Initialize them in the Demo() constructor:
Code:
graphics_ = load_bitmap("graphics.bmp", 0);
worldmodel_ = new WorldModel(BOARD_COLUMNS, BOARD_ROWS);
worldview_ = new WorldView(worldmodel_, grid_, graphics_);

Cleanup in the ~Demo() destructor:
Code:
delete worldview_;
delete worldmodel_;
destroy_bitmap(graphics_);

and call the View's render method in the Demo::render() method:
Code:
worldview_->render(target);

That was simple right?
If you run your program now..it will crash.
Why? Because you don't have the graphics.bmp file...duh.
You can download the file by right-clicking and saving the following image:



The tiles are in order of left to right:

Floor (unused because we currently use the Grid's display for the floor)
Wall
Goal
Box
Player

All tiles are 32x32 (remember we set up the Grid to be 32x32? well this is why...)

So once you have that and you run the program, it should do the exact same thing as before...
because the tile map is all zero right now, and we don't render any tiles with zeros...
so it doesn't display anything!
Next I'll show you the next structure in this project, for world generation.
Until then...
Logged

Richard Marks
202192397
Administrator
Member
*

Respect: 3425
Offline Offline

Posts: 1027


happy


« Reply #6 on: November 03, 2009, 04:24:01 PM »

I'm back! And now lets get to the world generation already! Grin

The struct name is rather obvious... WorldGenerator.
All that this struct needs to do is modify a world model for us, so we just need a pointer to one.
Our struct looks like this thus far:
Code:
struct WorldGenerator
{
WorldModel* model_;
WorldGenerator(WorldModel* model) :
model_(model)
{
}

~WorldGenerator()
{
}
};

I've decided to implement this as individual methods for each type of generation.
Right now, all that exists is a very simple "room" generator.
It clears the world tile map, and adds walls around the edge.

Code:
void basic_room()
{
model_->clear();

int width = model_->get_width();
int height = model_->get_height();

for (int y = 0; y < height; y++)
{
model_->set(0, y, 1);
model_->set(width - 1, y, 1);
}

for (int x = 0; x < width; x++)
{
model_->set(x, 0, 1);
model_->set(x, height - 1, 1);
}
}

Now lets see how we use this new struct.
In our Demo() constructor, right after we init the worldmodel_ variable, we call our generator:
Code:
WorldGenerator gen(worldmodel_); gen.basic_room();

That was simple. Cheesy

Now, when you run the code, you should get the basic room displayed.
Feel free to add more generators for different things.

Oh right, before I forget..I added one more thing to the WorldGenerator struct.
A token spawn method.
(Or in other words, a method that randomly places a specific value on the map, but never overwrites existing values.
So only on a zero will the "token" be placed. Useful for creating random rooms.
Code:
void spawn_token(int token)
{
int width = model_->get_width();
int height = model_->get_height();
bool ok = false;
int spawnx = 0;
int spawny = 0;
while(!ok)
{
if (!ok)
{
spawnx = Random::in_range(2, width - 2);
spawny = Random::in_range(2, height - 2);
int value = (*model_)(spawnx, spawny);
if (!value)
{
ok = true;
}
}
}
model_->set(spawnx, spawny, token);
}

Before I go, let me show you where to use it...in the basic_room() method!
The last thing in the method should be:
Code:
this->spawn_token(2);
this->spawn_token(3);

That spawns 1 box and 1 goal in the room. Cool

Next up I'll talk about the player.
Logged

Tags:
Pages: [1]   Go Up
  Print  
 
Jump to:  

Powered by MySQL Powered by PHP Powered by SMF 1.1.21 | SMF © 2015, Simple Machines Valid XHTML 1.0! Valid CSS!