#109377 - Diirewolf - Fri Nov 17, 2006 8:57 pm
How would you organize your code in a Gameboy Advance game... ie. which classes would you have, what data would they need to contain? which global functions and variables will you need? How would your loop be? How would you implement timing (other than direct coding into the Timer interrupt handler)? etc?
#109379 - keldon - Fri Nov 17, 2006 9:23 pm
I think the best thing you can do is start from the beginning; get your programming skills up to a certain standard and you will find that the answers to these sorts of questions becomes obvious.
There is a typical way of handling it, but it is good to know the basic programming principles first.
#109433 - Diirewolf - Sat Nov 18, 2006 8:32 am
you see i am already an experienced programmer, and ive made games using C# and managed DirectX, but I want to move to gameboy advance and I have read a lot on the hardware and am pretty fluent on how it works. I have made some simple demos but I cannot seem to make a full game (not even pong) because Im not sure how a gameboy game would be properly structured codewise. please provide me with some advice or guidance.
thanks
#109435 - Dwedit - Sat Nov 18, 2006 8:47 am
There's always the old structure of iterating through an object table and calling a handler for each game object. The handler moves the object around, or handles its behavior or AI or whatever. I've made games with that structure, and appearently, so have some commercial developers.
Usually it's like this:
do
* Get input
* handle objects
* draw
loop
Objects don't correspond to anything built into a programming language, no c++/java classes or anything like that, just maybe at best an object can turn in to a C struct. It's data in an array, with some common fields in standardized positions, like coordinates or graphic, or object type.
The disadvantage to the array approach is that it's an array. You can get holes as objects are removed, and you have to iterate through many objects to do collision checking. But those issues can be programmed around, maybe by forcing no holes, and building tables to eliminate useless collision tests.
I also have source files for other stuff that doesn't pretain to the main game loop, like the frontend.
One thing I haven't done yet is separate the animation logic from the game logic, that would probably be very helpful, and I can't imagine now doing a game without doing that.
_________________
"We are merely sprites that dance at the beck and call of our button pressing overlord."
#109437 - keldon - Sat Nov 18, 2006 9:29 am
Diirewolf wrote: |
you see i am already an experienced programmer |
So you have a keen understanding of software engineering and design patterns?
Markup Fairy was here
#109453 - sgeos - Sat Nov 18, 2006 11:42 am
First, put all of the code that directly mucks with hardware (I/O registers, VRAM, etc) in a library of some sort.
Next, your basic loop is going to look something like this:
Code: |
struct program_state state; // all program specific stuff
init_program_state(&state);
while (FOREVER) // loop
{
wait_until_next_vblank(); // timing
update_display(&state);
get_updated_input(&state);
update_game_state(&state);
} |
Never use global variables. Ever. Module level variables are OK (global but only visible to one module); true globals are never OK. I guess hardware service modules could be built with "global functions". That is probably poor OO design, though. (If good OO design matters to you.)
-Brendan
#109459 - Diirewolf - Sat Nov 18, 2006 1:24 pm
can you give me a rough outline of what data the program_state struct contains, and an example of one of the functions that use it eg. init_program_state(&state);
tx
#109460 - enigmacairo - Sat Nov 18, 2006 1:25 pm
Quote: |
Never use global variables. Ever. Module level variables are OK (global but only visible to one module); true globals are never OK. I guess hardware service modules could be built with "global functions". That is probably poor OO design, though. (If good OO design matters to you.)
-Brendan |
Er, why on earth not use global variables? As an experienced game developer with over 20 titles under my belt, i've never heard of that particular school of thought...
#109472 - Diirewolf - Sat Nov 18, 2006 3:57 pm
Quote: |
Er, why on earth not use global variables? As an experienced game developer with over 20 titles under my belt, i've never heard of that particular school of thought... |
Actually he's got a point. I program using C# and everything is in classes/modules (OOP) and it works fine, you don't need globals really. This is actually what is confusing for me because Im trying to make a game in c++ and i program games mainly using c#.
#109474 - sgeos - Sat Nov 18, 2006 4:02 pm
Diirewolf wrote: |
can you give me a rough outline of what data the program_state struct contains, and an example of one of the functions that use it eg. init_program_state(&state);
tx |
It's contents are really going to depend on what you making:
Code: |
// tic-tac-toe
#define BOARD_W 3
#define BOARD_H BOARD_W
#define BOARD_CELLS (BOARD_W * BOARD_H)
#define EMPTY_CELL 0
typedef struct program_state
{
int frame_counter; // can be used for animation
int board[BOARD_CELLS];
} program_state;
void init_program_state(program_state *pState)
{
int i;
for (i = 0; i < BOARD_CELLS; i++)
pState->board[i] = EMPTY_CELL;
pState->frame_counter = 0;
init_all_libraries();
}; |
For something more complex, the program state might be a container for other structs and tables of structs, unions and whatnot. It might actually want to be a union depending on what you want to do. It just depends.
Note that library variables and all that good stuff belong in their own modules. They don't belong in the program state. The RNG's state lives in RNG module. The RAM based map buffer belongs in a display module.
enigmacairo wrote: |
Er, why on earth not use global variables? |
First, I'll note that global constants (ie, macros) are not global variables. Macros are very useful. I could see cases where you might replace a macro with a variable. This wouldn't break existing code, and it would work. It would also confuse me to no end if I had to maintain that code, because I expect global constants to be constant. Probably not what you had in mind.
True globals are sign of a bad architecture. Functions, in general, should be passed all relevant values instead of fetching them from the ether. True globals can be changed anywhere. Only the original author knows where they change, and that is if he remembers. A maintainer don't know what the active scope really is. The variable could potentially be changed anywhere in any module. The maintainer doesn't know what the active scope really is. The only way to find out is to read the entire source code listing for the entire program- saving the game could affect a global... playing a sound effect could affect the same global... collecting an item could change it yet again. All in one frame. This is the biggest issue.
The next issue is, "where did this variable come from?"
Code: |
item_t get_spawn_item(int spawn_rate)
{
if (random(0, MAX_SPAWN_RATE) < spawn_rate + luck)
return random(0, MAX_ITEM);
return ITM_COIN;
} |
"luck" pops up out of nowhere. What is it? Where is it? How is it initialized? In a longer function, I might not realize that luck isn't declared in the function until I need to fix that function. This function is small, so I know that luck is declared elsewhere. The "what is it" problem can be solved with proper naming conventions:
Code: |
item_t get_spawn_item(int pSpawnRate)
{
if (random(0, MAX_SPAWN_RATE) < pSpawnRate + gLuck)
return random(0, MAX_ITEM);
return ITM_COIN;
} |
Now at least I know that it's global, but where is it declared? If I need to know, I'll have to run up to the top of the file. If it's at the top of the file, great. If it's an external global variable, I'll have to track it down. That will suck. I mean, it will really really suck.
The solution: don't use external globals. Given a program of any complexity, there is no variable that the entire program needs to be able to see. (I certain hope gLuck doesn't cause saveGame() to fail.) If only part of a program needs to see a variable, that variable is really a module level variable and not a global variable.
I frequently use module level variables. The state in an RNG is a good example. seed() and random() both touch the state, but nothing else should ever see it directly. (OK, I usually have getSeed() for when I really need to save the seed number.)
To a certain extent I could see putting asset LUTs in global space. Then again, only the modules that use those assets need to see them, so maybe they are just external module variables.
enigmacairo wrote: |
As an experienced game developer with over 20 titles under my belt, i've never heard of that particular school of thought... |
If you know when to use globals without sacraficing maintainablity, then you know when to do that. When globals are abused, they make code harder to maintain, so they are best avoided as a rule of thumb. (I'd probably sooner use a goto than a true global.)
-Brendan
#109508 - sajiimori - Sat Nov 18, 2006 10:06 pm
I call B.S. on the "zero globals" rule. Here are some globals in my projects:
* gActors, the actor manager. I don't want to pass an ActorList pointer to every function just because some deeply-nested function might want to broadcast a message to all actors. The pointer will be dead weight in most functions.
* gSolids, the collision object manager. Passing around a reference so everyone can do collision tests is pointless. Anyone should be able to do tests at any time.
* gScene, the 3D graphic object manager. A few modules need to create, update, and destroy the scene. Very few modules use it directly, yet it's global anyway! Making it a parameter simply doesn't solve a problem. In fact, it could be worse because when entering game mode X, you'd either have to know whether that mode uses the scene (which exposes X's concerns to the caller), or you'd have to pass the scene to all new game modes (which forces them to know about something they might not care about).
None of these offer a solution:
Code: |
void set_luck(int);
int get_luck();
|
Code: |
class GameState
{
public:
void set_luck(int);
int get_luck() const;
private:
int luck;
};
GameState* get_game_state();
|
Code: |
void some_function(GameState*, ...);
void another_function(GameState*, ...);
void every_function(GameState*, ...);
|
Here is something similar to what any module could potentially do in my last game:
Code: |
gLevel()->player()->setLuck(...);
|
It really doesn't matter how much you wrap it up: luck is still a global, and passing around a Level pointer to every function doesn't solve the problem of anyone being able to change luck at any time.
The problem can be "solved" by passing only the minimum amount of game state to every function, but it still results in dead weight for intermediate functions that have to forward it to lower-level functions, and it means all layers of abstraction have to concern themselves with what only lower-level code may care about.
Additionally, objects that require access to would-be globals would each have to permanently store an extra pointer to that data, because they'd have no way to recover the data from the global namespace if it were lost.
Between these problems and the additional maintenance and complexity, this "solution" costs more than it's worth. If you're really worried about it, make the data private and declare a list of friends that should be able to modify it. However, this requires lower-level modules to be aware of higher-level ones in order to give them permission, and inverting the dependency tree tends to have costs of its own.
#109512 - tepples - Sat Nov 18, 2006 10:32 pm
sajiimori wrote: |
I call B.S. on the "zero globals" rule. Here are some globals in my projects:
* gActors, [a singleton].
* gSolids, [another singleton].
* gScene, [another singleton].
|
But do those need to be singletons? Wouldn't it be possible to have more than one scene (consisting of a set of actors, a set of solids, and one or more 3D views) that can be drawn, collided in, or acted in?
Quote: |
Additionally, objects that require access to would-be globals would each have to permanently store an extra pointer to that data, because they'd have no way to recover the data from the global namespace if it were lost. |
Wouldn't you need an extra pointer to that data anyway so that the object can tell which scene it belongs to?
_________________
-- Where is he?
-- Who?
-- You know, the human.
-- I think he moved to Tilwick.
#109524 - sajiimori - Sun Nov 19, 2006 12:13 am
Tepples, your post has a bizzarre moebius-strip-like quality. You asked why they need to be singletons, and then said if they weren't singletons then they'd need extra pointers anyway, so they may as well have extra pointers, because if the managers weren't singletons then the pointers would be needed, because the managers may not need to be singletons, because...?
My head hurts. :P Let's untie that knot: The extra pointers are a cost of supporting multiple manager instances, and one might ask whether the benefit of multiple instances outweighs that cost along with any others. (Answer: It depends.)
Anyway, I can talk about why I made them something like singletons.
gScene needs to be restricted to a single instance because it initializes the 3D hardware and VRAM allocator upon construction. In fact, the only reason it's an instantiatable object (as opposed to static functions and data) is to guarantee initialization and cleanup at the correct times. Models can already be viewed from their own custom cameras (e.g. for having multiple 3D windows), so there's no reason to have extra Scene objects.
It's already possible to instantiate more than one solid manager, but each type of solid will be a different static type, so it will know which manager is associated with it and the extra pointer isn't needed. However, solids can already be grouped into categories, so there's little point in creating more managers; it's usually better to just separate out a new category in the same manager.
Having multiple actor lists is more of a game-level decision. Actors are certainly divided into groups, and putting them into different managers is one way to implement those groups. One situation where it might make sense is if the pause menu was implemented using actors, and you want to update the menu actors while paused but not the game actors. There's more than one way to cut that cookie, but the question of whether to make the managers global is a separate issue.
#109535 - sgeos - Sun Nov 19, 2006 2:28 am
sajiimori wrote: |
* gActors, the actor manager.
* gSolids, the collision object manager.
* gScene, the 3D graphic object manager. |
These stirke me as something very close to library style service modules. Yes, passing a math object from the top level to everything that needs to use sin() and cos() is silly. So is passing the random number generator (usually). At some point you are going to have to create service packages. Fine. The package might need to retain it's state. Fine.
Do these services need to be visible everywhere? I find it difficult to believe that they will be needed in every other module. When is the last time the sound player had to muck with actors? Why is the sound player mucking with the actors? Or checking for collisions? Or mucking with the scene?
sajiimori wrote: |
None of these offer a solution:
*SNIP examples* |
Because we don't know what luck is. Is luck the player's luck? The luck of the world? The difficulty level? The number of lucky items left in the game? Is it something else entirely? Presumably that is documented with the variable. *goes off to check* The goal is to have luck live with companion variables and code- not off in the ether somewhere.
tepples wrote: |
But do those need to be singletons? Wouldn't it be possible to have more than one scene (consisting of a set of actors, a set of solids, and one or more 3D views) that can be drawn, collided in, or acted in? |
Sure, although it has probably been deemed unlikely or impossible on the current hardware/game. If I really need to run two copies of the game at once, I probably have a completely different game. Will I want to do that in the future? Probably not. If I do, I guess I'll cross that bridge when I get there.
IIRC, a properly implemented singleton pattern can be converted to a non-singleton architecture with relative ease. I'm not conviced that putting the theory into practice is as painless as it is made out to be. (Although maybe the singleton wasn't "properly implemented".)
-Brendan
#109537 - sajiimori - Sun Nov 19, 2006 2:40 am
Quote: |
Do these services need to be visible everywhere? |
I thought you just agreed that passing around sin() and cos() is silly, despite the fact that not every module uses them?
The fraction of modules that use a given piece of data is not the only (or even the most important) factor in deciding whether it should be global.
Does the sound library use gActors? No. Do I want to pass the actor list through all the game logic modules? Hell no. It doesn't solve a problem. Doing it just to avoid having a global is wishy-washy idealism for its own sake.
Quote: |
The goal is to have luck live with companion variables and code- not off in the ether somewhere. |
All my examples tie together related code and data, but the data is still global. If all you want is to tie code and data together, you're not arguing against globals -- you're just arguing for keeping your globals organized.
#109549 - Ant6n - Sun Nov 19, 2006 7:11 am
sajiimori wrote: |
Quote: | Do these services need to be visible everywhere? | I thought you just agreed that passing around sin() and cos() is silly, despite the fact that not every module uses them?
|
I think the point sgeos was trying to make is that your gActors acts much like a library. You decided to declare it as a singleton global class, while others would have created something like lib_actors (its just a somewhat different paradigm, but it comes down to knowing its some kind of big package). I think a library that can only have one instance is conceptually not exactly equivalent to a global variable, even if the implementations are similar.
The example with the "luck" probably does not refer to some kind of package or library; it is probably some kind of singleton datum sitting somewhere and nobody really knows what it is. If you have something like that it would probably be better to go back and look at the general design of the software and find a more purposeful way to place it in the overall design. for example place it in the library which is most suited to take care of it. (it is similar to having to do typecasting in a well designed oop-language like java, because one has screwy subtypes/supertypes as a result of bad class design)
I think this mystical 'overal design' is what the original question was about, and i'd be more interested to hear about that than this ideological war between different paradigms.
#109550 - sgeos - Sun Nov 19, 2006 7:16 am
sajiimori wrote: |
Quote: | The goal is to have luck live with companion variables and code- not off in the ether somewhere. | All my examples tie together related code and data, but the data is still global. If all you want is to tie code and data together, you're not arguing against globals -- you're just arguing for keeping your globals organized. |
The goal is to keep the program organized. If the no globals rule of thumb seems broken, you probably know when to break it.
As far as "wrapped globals" go, they both are and are not global. For example, you could:
Code: |
void some_function(program_state *pState)
{
// ...
pState.clone();
// ... |
Useful? Probably not unless you want to implement a memory resident save state system. A real memento system or writing to disk might work just as well.
You could implement a system like this:
Code: |
int main(void)
{
program_state state[STATE_MAX];
int i;
for (i = 0; i < STATE_MAX; i++)
init_program_state( &state[i], i ); // presumably creates a new window
while (FOREVER)
{
wait_for_next_frame();
for (i = 0; i < STATE_MAX; i++)
update( &state[i] );
}
return 0;
}
void init_program_state(program_state *pState, mode_t pMode)
{
switch (pMode)
{
// mode specific initialization
}
}
void update(program_state *pState)
{
update_view ( pState );
update_control( pState );
update_model ( pState );
} |
Is that useful? Maybe. For a game running on the DS? No, not really. If the game states need to share resources, you are probably looking at something like this:
Code: |
typedef struct program_state
{
shared_state shared;
sub_program_state state[STATE_MAX];
} program_state; |
Which is the same as just having a single state to begin with. I suppose some sort of indexed state messaging system could be used, but it sounds horrible to me.
Naturally, these kinds of systems break if anything important lives in the ether.
I guess that's a long way of saying that globals are OK, but they need to be kept organized. You also have to be sure that you'll only need one copy, ever.
-Brendan
#109555 - sajiimori - Sun Nov 19, 2006 8:33 am
Quote: |
I guess that's a long way of saying that globals are OK, but they need to be kept organized. |
Since I was only taking issue with the claim that globals are always bad, I guess we're in agreement. Quote: |
You also have to be sure that you'll only need one copy, ever. |
Or rather, in your current design. Trying to design for everything you could ever want is a sure-fire route to an overengineered program. :)
#109561 - sgeos - Sun Nov 19, 2006 9:41 am
sajiimori wrote: |
Quote: | I guess that's a long way of saying that globals are OK, but they need to be kept organized. | Since I was only taking issue with the claim that globals are always bad, I guess we're in agreement. |
Nothing is always bad.
sajiimori wrote: |
Quote: | You also have to be sure that you'll only need one copy, ever. | Or rather, in your current design. Trying to design for everything you could ever want is a sure-fire route to an overengineered program. :) |
Remaining adaptable is good. Deciding what to simplify/sacrafice is a field decision that any good programmer should be able to make.
-Brendan
#109567 - keldon - Sun Nov 19, 2006 10:48 am
The conclusion is often less important as to the steps that reached it. If you have no reasoning behind your conclusion then you run the risk of being in agreement, or in the result of a conclusion that is faulty for this case. Which is why I believe it is fundamental to your programming that you have the core knowledge to make such a decision.
#109728 - Miked0801 - Mon Nov 20, 2006 8:15 pm
And I thought you were supposed to use globals for everything. Silly me. :)
The main reason globals are taught to be "evil" is that they are very easy to abuse and turn into evil problems. Nothing like haivng a global pointer in one module, malloc/new it in another, and directly manipulating crap behind it in a third (assuming that it exists already). Then handing the mess off to a half-dozen other programmers and watching the global timebomb explode.
Globals, like (god forbid) Gotos solve unique problems, but need to be used sparingly and with documentation to support their use.
#109749 - Miked0801 - Mon Nov 20, 2006 11:35 pm
BTW, on your sig
2m 55s, 97 cap :)