Author Topic: new GameSettings model  (Read 3091 times)

daniel.santos

  • Guest
new GameSettings model
« on: 5 April 2009, 06:08:34 »
I was going to post this is the networking re-write thread, but I it includes other topics that have been often discussed elsewhere so I decided to post a new thread instead.

As part of the network re-write, I've taken some time to consider the multitude of suggestions, ideas, requests, etc. that have come across the boards and also what I have seen in other games and like, but never got around to posting.  I would rather think something through and do it in such a manner so that I don't have to "break" it again than to do it quickly, but end up having to do more re-writes later when expansion/changes are needed.  Thus, here is the new GameSettings model.  First off, the C++ class Game::GameSettings is what is generated when you start a new game (local or network).  It tells what map you're using, who is in each player slot, etc.  I'm omitting the unimportant details here (there are more members to the classes in the diagram below).  This is using UML (Unified Modeling Language) notation.



This new model reflects some major changes from how Glest/GAE is currently implemented.  These changes will be in 0.2.12 at the GameSettings level, but most will not be implemented until later on (some much later on).  However, doing it correctly now circumvents the need to revisit it later.  So for clarity's sake (and those who don't understand UML), this is what it means  (and some of this is mundane):
  • Each game consists of one or more teams (you don't have much of a game with only one team, but hey, you're free to play with yourself if you choose :) ).
  • Each team has one or more factions belonging to them.
  • A Player is an abstract class that represents either a human or AI player, represented by the subclasses HumanPlayer and AIPlayer, respectively.
  • A FactionController just associates one faction with one player. EDIT: I have removed this concept from the design.
  • Each faction must be associated with at least one FactionController.
  • However, a player isn't required to be associated with any FactionControllers (these players are now just not assigned to any faction and their spectator flag is set to true), in which case they are a spectator who can observe the game and has full visibility, but cannot chat privately with anybody (only open chat).
  • So in essence, each faction can be controlled by one or more players and each player can control one or more factions!  Generally, a player shouldn't be allowed to control factions that aren't on the same team (although it will probably be allowed when compiled for debug).
  • There will be a restrictions of a max of one AI player per faction because having more than two is just going to screw things up.  But what this means is that you can play the game with an AI to help you out (I got that idea from playing Rise of Nations).
  • At this time, the AIPlayer class will only specify rather the "CPU" or "CPU Ultra" AI is used.  In the future, this will be the class that contains the AI configuration that has been examined and discussed elsewhere on the forums.  Although AI is a bit OT for this thread, I'm personally leaning towards implementing AIs in the game code (in C++) and providing an interface & mechanism to implement them using scripting languages (Lua, etc.).  Probably, it will prove helpful in the future to support more than one scripting language, but I'm not even going there today.

So that's it.  Please post any feedback and thanks.  Also, remember that I'm not going to be digging deeply into AI and shared control right now unless it affects the above design as this exercise is part of the networking re-write and that's my primary focus at this time.
« Last Edit: 18 September 2009, 07:18:01 by daniel.santos »

hailstone

  • Local Moderator
  • Battle Machine
  • ********
  • Posts: 1,568
    • View Profile
Re: new GameSettings model
« Reply #1 on: 6 April 2009, 02:31:04 »
I haven't had a detailed look but I see HumanPlayer has NetworkInfo. Does this mean that you can't have a network game with 2 AI players and no human players?
Glest Advanced Engine - Admin/Programmer
https://sourceforge.net/projects/glestae/

bork

  • Guest
Re: new GameSettings model
« Reply #2 on: 6 April 2009, 04:16:55 »
Could you please give more details on what is the purpose of FactionController? I mean, isn't it possible to reference factions directly from player?

I'm not sure, but wouldn't it be more natural that the sole difference between HumanPlayer and AIPlayer is use of different command sources/sinks? I.e. for HumanPlayer you'd have Player instance and NetworkCommander (or like) instance attached to it and for AIPlayer you'd have just another Player instance and AICommander instance attached to that other other Player instance. This way should give you better decoupling of player related stuff and AI or network interaction stuff. And, well, it will let you have network games with AI players only (although, I don't see why you might want this :))


daniel.santos

  • Guest
Re: new GameSettings model
« Reply #3 on: 8 April 2009, 20:47:38 »
I haven't had a detailed look but I see HumanPlayer has NetworkInfo. Does this mean that you can't have a network game with 2 AI players and no human players?

My thought was that the AI player only needs to really *be* on the server, so I didn't give them any network information since it's presumed that the server takes care of that, although the client will continue to execute their own AI such that it will predict the server AI's behavior.  You can have a network game with 2 AI players and at least two HumanPlayers who are spectators.  I haven't thought out any functionality to allow AIs to run on an alternate machine however because I figured the server should be "in charge" of that.  Even if somebody else has their own custom AI script, I'm thinking we should require that to be on the server as well (and the whole checksum process to make sure they're using the same version of the script, etc.).

Could you please give more details on what is the purpose of FactionController? I mean, isn't it possible to reference factions directly from player?

Yea, well you're problem is that you're just too damn good! ;)  Yes, when I actually went about implementing it I indeed did exactly as you suggest! :)  I guess when I was designing this I was thinking more from the standpoint of how it would look when set across the network as a message.  Of course, remember that these are the game settings (for the most part).  So now the GameSettings::Faction class has a "Players players" data member (Players is a typedef of vector<Player*>).  Additionally, the GameSettings object has a member of the same type and name as well that contains all players.  So the players object of GameSettings contains all players, AI and human, and the GameSettings::Faction players data member contains all of the players that control that faction, with the primary always being first.  The distinction of primary is important because of some of the game settings that affect how units from that faction are managed (namely the GsAutoRepair and GsAutoReturn from the .ini file, but this could expand in the future).

As far as something to point from the Player object  back to the factions they control, I honestly hadn't gotten that far yet so I'm not certain if it will be needed.  However, the Faction class in the Game namespace (i.e., the "real" faction class) now has a "Players &players" data member as well.  I'm not 100% certain of the design just yet (as far as creational patterns and object life-cycle) but I think that Game::Faction objects will keep a reference to either the the Player object that GameSettings::Faction owns or, or just the GameSettings::Faction object its self (which is essentially the settings for that faction) as the GameSettings object will exist for the duration of the game and should, thus, remain valid.  Part of my thoughts on this is that midway through a game, a player can drop off.  If there is more than one player controlling a faction, then perhaps they will just continue to play with less players on that faction.  Of perhaps the human player is replaced by an AI player.  Either way, this means that we need to change the GameSettings object mid-game so it needs to be dynamic throughout the game's lifetime.  Thus, I would rather the data be kept in one place and have the Game::Faction object simply refer to that directly.

I'm not sure, but wouldn't it be more natural that the sole difference between HumanPlayer and AIPlayer is use of different command sources/sinks? I.e. for HumanPlayer you'd have Player instance and NetworkCommander (or like) instance attached to it and for AIPlayer you'd have just another Player instance and AICommander instance attached to that other other Player instance. This way should give you better decoupling of player related stuff and AI or network interaction stuff. And, well, it will let you have network games with AI players only (although, I don't see why you might want this :))

hmm... I think the short answer is that there is a completely different class hierarchy to manage command generation & processing.  The Game::Player and derived classes are primarily used to communicate player information from one host to another, although they are used outside of networking as well (which is why they are in the Game namespace and not Game::Net).  Commands by the local human player are generated via clicks & key input as processed by the Game::Gui class, although a few things related to game settings (pause, speed changes, etc.) that will generate network traffic are managed in the class Game::Game (Gui's base class).  When the Game's update() method is called, one of the things that it does is to run each AI by calling AiInterface::update(), which creates commands for the AI.

Commands are never executed immediately, they are always queued up and then processed when the World object gets it's turn to update.  I may have inadvertently removed the code from World::update() that tells the network interface to update, but that's being changed anyway.  When a world update starts, the network interface will have it's beginFrame() method called and when the update is completed, it's endFrame() method will be called.  The beginFrame() method retrieves & processes any complete network messages and shoves any commands received into the Commander object.  During the world update, auto-commands are commonly generated (auto-attack, auto-repair, auto-flee, etc.) and these are given to the Commander as well.  When the game is a network game, the Commander hands them to the network interface (Game::Net::GameInterface actually).  Thus, at the end of the world update, all of these commands plus any commands that were issued via the user interface or AI (when the Game object was updating) are all sent at once. Starting in version 0.2.12, they will be sent with a target frame (which will later be replaced by a target game time).  This is to ensure that all players in a network game execute the command at the same time (it's even queued up on the machine of the person who issued the command until that future frame arrives).

So, that is how it works.  If you see some ways to refactor this, please do share.  I would venture to say that there is some key functionality in the World and UnitUpdater classes that could be de-coupled, but I haven't been able to isolate it yet because those two classes are such catch-alls.  I would say that in the time I've worked on this project, 90% of the problems that I've had after making a change to the game are due to not fully understanding how these two classes behave (as they tend to encapsulate many of the rules of the engine).

While I'm on the topic of re-factoring, let me mention of few items on my re-factoring agenda :)
  • Get rid of all cases where empty constructors are called and an init() or load() is later called.  I want these to have proper constructors that initialize all of their members correctly the first time.  It may not always be possible to kill the empty constructor, but in this case, I want it as close to that as possible.
  • If a classes can render its self as xml, I want it to inherit from a new class Shared::Xml::XmlWritable.
  • If it can be initialized from xml, I want it to have a constructor that accepts a const XmlNode & (followed by any other parameters it may need to initialize).  If a class's objects are supposed to be able to re-initialized, then it's fine to have a separate load(const XmlNode &) function that the constructor calls, otherwise, the code to read from Xml should only be in the constructor.
  • Eliminate passing around pointers as much as possible in favor of references (they are harder to screw up).
  • For objects we have to allocate, I want to start moving towards using smart pointers (I've already had to start doing this with GameSettings because it was just getting unwieldy trying to manage who owned a particular GameSettings object at a give point in time).
  • All enums and other constants should follow standard C/C++ naming conventions (i.e., the value names should be UPPER_CASE)

daniel.santos

  • Guest
Re: new GameSettings model
« Reply #4 on: 8 April 2009, 21:28:26 »
Oh yes, this is a bit OT, but I have one more re-factoring item.  I prefer setters in the form of:
Code: [Select]
void setMyAttribute(type v) {myAttribute = v;}as opposed to
Code: [Select]
void setMyAttribute(type myAttribute) {this->myAttribute = myAttribute;}
As far as I'm concerned, when we're calling set followed by the proper-cased name of the data member, there's no need to re-iterate this in the parameter name and it just ends up making the text of the code larger (even though it will compile exactly the same).  Thus, I prefer to just use "v" for "value" and keep it brief.  When the operation is any more complicated than setting the value of a single data member, then please do use the corresponding parameter names instead of v1, v2 or some such.

bork

  • Guest
Re: new GameSettings model
« Reply #5 on: 11 April 2009, 20:37:31 »
[snip]

Could you please give more details on what is the purpose of FactionController? I mean, isn't it possible to reference factions directly from player?

[snip]

 :o That was a LOT more details :), thank you so much for the info it was very insightful. I think, I don't have enough understanding of all ins and outs to refactor the World (mm... I like how it sounds :) ) right now.

[snip]

While I'm on the topic of re-factoring, let me mention of few items on my re-factoring agenda :)
  • Get rid of all cases where empty constructors are called and an init() or load() is later called.  I want these to have proper constructors that initialize all of their members correctly the first time.  It may not always be possible to kill the empty constructor, but in this case, I want it as close to that as possible.
  • If a classes can render its self as xml, I want it to inherit from a new class Shared::Xml::XmlWritable.
  • If it can be initialized from xml, I want it to have a constructor that accepts a const XmlNode & (followed by any other parameters it may need to initialize).  If a class's objects are supposed to be able to re-initialized, then it's fine to have a separate load(const XmlNode &) function that the constructor calls, otherwise, the code to read from Xml should only be in the constructor.

Looks reasonable, but I think, you are not going to restrict classes which can be initialized from xml to be constructible from xml only? Looking from somewhat far and idealistic view point it might be better to separate class loading/saving in particular format (be it xml, or database or anything else). This conforms with single responsibility principle and increases encapsulation, which gives us simplified unit-testing, better chance of re-use, better understanding of class design and so on. On the other hand, it might be more practical to let classes work with xml by themselves for some time.

  • Eliminate passing around pointers as much as possible in favor of references (they are harder to screw up).
  • For objects we have to allocate, I want to start moving towards using smart pointers (I've already had to start doing this with GameSettings because it was just getting unwieldy trying to manage who owned a particular GameSettings object at a give point in time).
  • All enums and other constants should follow standard C/C++ naming conventions (i.e., the value names should be UPPER_CASE)

Just my 2 cents about last point. What is "standard C/C++ naming conventions"? I see a lot of different conventions, and I don't think there could really exist a standard one. I always thought that coding style main point is consistency, not in particular rules you use. So there should be no much difference whether  you call something 'LikeThis' or 'LIKE_THIS' as soon as you do it consistently through over your project. It's all up to you, of course, but why change something such small and irrelevant like casing of enums and constants?

daniel.santos

  • Guest
Re: new GameSettings model
« Reply #6 on: 12 April 2009, 09:02:57 »
Looks reasonable, but I think, you are not going to restrict classes which can be initialized from xml to be constructible from xml only? Looking from somewhat far and idealistic view point it might be better to separate class loading/saving in particular format (be it xml, or database or anything else). This conforms with single responsibility principle and increases encapsulation, which gives us simplified unit-testing, better chance of re-use, better understanding of class design and so on. On the other hand, it might be more practical to let classes work with xml by themselves for some time.
Hmm, I'll have to think on this.  I must admit that I don't like a lot about this mechanism because it means that if I want to change a class, I have a lot of code to change for it's XML serialization/deserialization.  On the other hand, I like stuff to be as efficient as possible, but not to the point where it starts looking like somebody threw up in the code editor.  I know that when I was using Hibernate with Java, there was some nice mechanisms to add some Java-5 annotations and Hibernate would pick that up and know everything it needed to doing serialization.  I'm certainly open to some input here.

Maybe you can take a look at what I'm doing in the game/network/network_message.h/cpp and let me know what you think.  Here, I'm actually breaking the rule of "always initialize objects properly" because my constructors that accept NetworkDataBuffer object always call the no-arg constructor of their base class, which leaves most data members un-initialized, with the idea that the read method will be called and propagate through the base classes to initialize data members.  Frankly, I'm not too happy with the overall scheme of this, it feels kinda difficult to maintain and it seems too easy to implement this interface incorrectly.  Maybe there's only so many safe-guards you can put into the design.  On the bright side, it's a pleasantly efficient way to serialize and deserialize objects to and from a binary data stream (also, the NetworkDataBuffer class manages machine word alignment with the ntohx and htonx functions, so that game play machines of different endianness should work).  Even though the read and write methods are virtual, the code in network_message.cpp always calls then with the explicit class name, which makes the eligible for inlining when compiled -O3, although the purpose of doing so is for correctness.

So just to re-cap, from what I can remember right now, there are 3 different types of this kind of object representation:

Binary -  Implementing the Shared::Platform::NetSerializable interface, err, inheriting from the Shared::Platform::NetSerializable class I mean :)
Code: [Select]
class NetSerializable {
public:
virtual ~NetSerializable() {}
virtual size_t getNetSize() const = 0;
virtual size_t getMaxNetSize() const = 0;
virtual void read(NetworkDataBuffer &buf) = 0;
virtual void write(NetworkDataBuffer &buf) const = 0;
};
The getMaxNetSize() is there so that the buffer can be sure to allocate the proper amount of room because it doesn't behave as a true data stream, so this interface is somewhat strict and a bit rigid, but also very efficient.

XML - Implementing Shared::Xml::XmlWritable
Code: [Select]
class XmlWritable {
public:
virtual ~XmlWritable() {}
virtual void write(XmlNode &node) const = 0;
};
This one one is very flexible, but of course, less efficient.  Also, I'm going by the assumption that you'll have a constructor that accepts "const XmlNode &" and you can't exactly require that in an interface.

Text - Implementing Shared::Util::Printable
Code: [Select]
class Printable {
protected:
Printable() {}

public:
virtual void print(ObjectPrinter &op) const = 0;
string toString() const;
};
This mechanism actually exists for debugging and it's very handy for that purpose (especially for network conversations).

As for the enums, I guess you have a bit of a point there, it's just always been confusing to me because I'm very used to a naming convention where constants are in UPPER_CASE.  Of course, I've done java for many years and that is the de-facto standard there.

Thanks so much for your input and consideration! :)

hailstone

  • Local Moderator
  • Battle Machine
  • ********
  • Posts: 1,568
    • View Profile
Re: new GameSettings model
« Reply #7 on: 14 April 2009, 02:56:51 »
I've added a section on the wikia for GAE Coding Conventions
« Last Edit: 18 June 2016, 14:06:01 by filux »
Glest Advanced Engine - Admin/Programmer
https://sourceforge.net/projects/glestae/

daniel.santos

  • Guest
Re: new GameSettings model
« Reply #8 on: 15 April 2009, 00:32:07 »
thanks!  This is an area that should *really* be expounded upon.  When 0.2.12 is done, I think I'll get on there and writing as much of it out as I can think of.  I will notate anything that doesn't have agreement and you can do the same. :)

 

anything