|
Prev: Name of construct
Next: Deriving - .NET example
From: H. S. Lahman on 16 Sep 2006 13:18 Responding to Guild... >>Basically what I am hearing here is that you want to make this >>game up as you go along developing the software. I don't think >>that works. >> >>All software needs requirements and all software developers need >>to understand the problem domain /before/ the software can be >>built. Here the problem domain is a fantasy realm. You have to >>have a clear idea of how that realm works (i.e., what the >>requirements are) before you start designing software. > > > Even so, the fact remains that large areas of the domain are fluid > and uncertain. I know for a fact that no matter how carefully I plan > the initial design I will need extensive maintenance. Surely you are > not suggesting that it is impossible to create software under those > conditions. Even though this is only a game, I cannot change the > domain. So the question is, what is the best way to use OO design to > assist me? What I am pushing back on is what business reasons lead you to expect that there will be a need for extensive maintenance? And if there is such extensive maintenance, what would be the nature of it? The main reason I am pushing back is because this doesn't seem to be the direction the gaming industry is going. First, the industry is clearly consolidating on game engines. Now almost all major games use one of a dozen (at most) game engines and it wouldn't surprise me if there was even more thinning of the herd because it is simply too expensive to develop game engines due to the high content of advanced graphics and physics, both of which should be reusable. More important, new game releases seem to be in two camps. One is the tweak camp where each major release just jazzes up the graphics and provides a bunch of new units that work the same way as the old ones but look different and have somewhat different effects on game play. The CIV series is a classic example of this where even a game that looked as different as Alien Crossfire basically worked exactly the same way as all the other CIVs. And all MMOGs are in this camp. The other is the rewrite camp where a completely different game is produced with each release. An example is Starfury vs. Space Empires. To do that one effectively redesigns and re-implements the game. This originated with clone games where I-can-do-it-better rules. However, I think a large consideration is that in good simulations the key is how things play together. So doing something better usually involves fixing a lot of little things that interact at the game design level so one effectively has a fundamentally different design to implement when one is done fixing a few things to make it better. What I don't see are new releases of games where much of the game works the same way but certain aspects of it are substantially changed. I've only seen two examples of that, MOO III vs. II and Galactic Civilizations II vs. I, and both were disasters. Both were largely the same but some areas were modified substantially. The result was that to the existing player base it looked like the same game but those players found that the old ways of doing things didn't work anymore, which led to a backlash from the installed base of players. [Interestingly in both cases the game elements didn't change much but the UI paradigm to manage game elements changed a lot. So it was not a matter of what one needed to do to be successful, but how one needed to do it.] > > Though I would not have worded it this way, until now my plan has > been to use an extremely abstract entity objects. For each one I > would represent physical properties as a collection of 'Property' > objects which are also extremely abstract, holding only a data value > and a 'PropertyType' reference to provide the semantics of the value. > The behaviour properties of the entities would be represented by a > fixed set strategy objects which polymorphically dispatched event > messages to low-level entity manipulation methods. > > I could implement my entire design in this way by creating strategies > until all the desired entities were behaving as desired, with no > difficulty in adding or modifying entities except for the complexity > of working on such a low level. > > Through reading this thread, I have come to realize exactly what I > need. Primarily, I need extremely high decoupling between entities. > It is a priority above all others that I should be able to add or > modify entities at will in any manner that satisfies the very > primitive fundamental structure of the domain without forcing even > the slightest modification to other entities. > > Towards this goal, each entity becomes like a subsystem with the > properties acting as the data interface, though it is data by > reference to accommodate the necessity of entities modifying each > other's properties. Also, entities will naturally share classes and > stateless objects, but otherwise they will be quite separate. > > Now the only problem is to overcome the complexity and maintenance > nightmares of dealing with the low-level property manipulation. I > suspect that this should be dealt with using your idea of role > objects. The only place lacking abstraction currently is within the > implementation of the entity behaviour, so let it be dealt with > independently. The behaviour strategy for each entity can assign > roles to each entity that it interacts with by wrapping the bare > Entity objects in more meaningful Role objects, such as Weapon, > Monster, Furniture, etc. > > Just as you suggested, when a Monster picks up a Jaws to attack with, > the Jaws will be wrapped in a Weapon object because it is now > conceptually a weapon. The Weapon object will implement some or all > of the low-level property manipulation that goes along with using a > weapon but in an abstract way. No other Entity object needs to know > about the existence of the Weapon object. > > I can use object-oriented design within the implementation of an > entity or even a large group of entities to greatly simplify the > otherwise low-level implementation, but that design is to be > encapsulated to always a subset of entities. > > If I find that my abstractions and generalization for my current > entities are not allowing me to create a new entity which I nee
From: H. S. Lahman on 16 Sep 2006 14:10 Responding to Guild... >>The problem is reading the attributes because they are not in >>[ConcreteA], they are in [ObjectA]. So the saveIt() method in >>ConcreteA must navigate the R1 relationship to get to them rather >>than the 'this' pointer. If R1 is implemented as a pointer, then >>the address of that reference with be exactly the same as the >>address provided by the 'this' pointer. The only difference is >>that the 'this' pointer in Serializer is hidden while the R1 >>pointer in the deconstructor approach must be explicitly >>implemented, instantiated, and navigated. >> >>In the Serializer case somebody had to "walk" the objects in >>[ObjectA] set and invoke the Serializer's saveIT() (or whatever) >>protocol method for each object. Somebody has to do the same >>thing for the deconstructor approach. When they do so, they must >>instantiate the R1 pointer to the current ObjectA in hand before >>invoking saveIt() to be sure saveIt() gets the right attribute >>values. > > > If I understand this correctly, the ConcreteA class will have only > one object and the persistence encoding algorithm walks through the > relationships of the objects to be encoded. Each time it encounters > an object it finds the appropriate 'saveIt' object, i.e. the lone > instance of ConcreteA or ConcreteB or ConcreteC, depending on the > class of the object to encode and using dynamic cast if polymorphism > is involved. Actually, I was sloppy in the model; the relationship should have been 1:1, not *:1. ConcreteA can save any member of the [ObjectA] class, but it only does it one object at a time. I assumed some other object would "walk" the collection of all [ObjectA] members, instantiate a 1:1 R1 for each one (so ConcreteA could get at the data), and then would invoke Deconstructor.saveIt(). The ConcreteA.saveIt() implementation would navigate R1 and read the right attributes to save. However, as I pointed out later in the message, the loop over [ObjectA] objects could be in Deconstructor.saveIt(). Then saveIt() would only be invoked once. > > Earlier, you said, "When you read the object type you can instantiate > the R1 relationship so that ObjectA is processed by the ConcreteA > deconstructor when saveIt() is invoked. Essentially what you have > done is moved the polymorphic dispatch from the object to be saved > into a GoF Strategy pattern." > > I am sure that the Deconstructor class and its subclasses are the > strategies, even though the GoF specifically say that the Context > object has a reference to its strategy. But I cannot see where > polymorphic dispatch is involved here. I presumed that it had been > sacrificed when we abandoned the Serializer pattern to avoid coupling > with the persistence. The polymorphic dispatch comes in when one used [Deconstructor] to save objects from [ObjectB] and [ObjectC] classes. Instantiating R1 correctly also means that one has to have the right flavor of [Deconstructor] subclass object in hand when one invokes Deconstructor.saveIt(). IOW, subclassing [Deconstructor] and using it to save other classes than [ObjectA] requires polymorphic dispatch of saveIt() going to the right flavor of [Deconstructor]. That is essentially the same as inheriting Serializer's saveIt() for the [ObjectA], [ObjectB], and [ObjectC] classes. One invokes the same interface but gets different implementations depending on which class one is saving. > > Assuming that you literally meant that the Decontructor was following > the Strategy pattern and therefore each ObjectA must have a reference > to the unique ConcreteA, then I have another interpretation of your > words: the factory does choose which Deconstructor goes with each > object and stores it in a reference for the life of the object, to be > used by the persistence algorithm and eliminating the need for > dynamic casts. The R1 relationship is navigated only one-way. The ConcreteA.saveIt() implementation needs to read the attributes in the right ObjectA but ObjectA doesn't need to know that [Deconstructor] exists at all. Corollary: when it comes time to save objects, whoever is running that show talks to [Deconstructor], not [ObjectA]. (It does need to instantiate R1 in [Deconstructor] so that Deconstructor.saveIt() can find the right ObjectA.) IOW, whoever is running the show tells [Deconstructor] to do the saving. <aside> Note that in the GoF patterns' example implementations the client talks to [Context], not [Strategy], and [Context] relays the request to [Strategy]. That is not good OOA/D practice because it violates peer-to-peer collaboration by making [Context] an agent or middleman in the collaboration. Once the delegation is defined, the Strategy owns the relevant properties (e.g., saveIt() in [Deconstructor]). So the client should talk directly to [Deconstructor]. Typically that would be done by the Client navigating a <generic> relationship path through [Context]. In this case, though, saving is likely to be an important part of the problem solution that is orthogonal to other collaborations where the Clients might need to talk to [Context]. In addition, the Client will probably be an object that explicitly encapsulates the rules and policies around saving objects. That all combines to make it rather likely that the Client would have a direct logical relationship to [Deconstructor]. Thus, instead of [OrdinaryClient] | 1 | | R2 | | related to | * [ObjectA] | 1 | saves | | R1 | | 1 [Deconstructor] we might have something like: [OrdinaryClient] | 1 | | R2 | | related to | * * manages persistence for [ObjectA] -------------------------------+ | 1 | R3 | saves | | | | R1 | | | | 1 1 invokes 1 | 1 [Deconstructor] ------------------ [SavingClient] R4 Here R3 captures the fact that whoever is running the show needs to know which objects need saving. Here it would be logical for [SavingClient] to have the loop over instances of [ObjectA]. However, one
From: Brendan Guild on 16 Sep 2006 20:17 H. S. Lahman wrote in news:j6XOg.1601$832.1014(a)trnddc04: > Responding to Guild... > > Actually, I was sloppy in the model; the relationship should have > been 1:1, not *:1. ConcreteA can save any member of the [ObjectA] > class, but it only does it one object at a time. I assumed some > other object would "walk" the collection of all [ObjectA] members, > instantiate a 1:1 R1 for each one (so ConcreteA could get at the > data), and then would invoke Deconstructor.saveIt(). The > ConcreteA.saveIt() implementation would navigate R1 and read the > right attributes to save. Now I realize that there is something else bothering me about that diagram. Let me draw an expanded version just to be sure that I understand correctly: 1 saves R1 [ObjectA] ------------------------+ | 1 saves R3 1 | 1 [ObjectB] ----------------- [Deconstructor] 1 | + saveIt() 1 saves R4 | A [ObjectC] -------------------+ | R2 +---------------------+-----------------+ [ConcreteA] [ConcreteB] [ConcreteC] Since the direction of these relationships is now clear, if I am correct then it must be that every Deconstructor object has references to ObjectA, ObjectB, and ObjectC, then the various concrete deconstructors choose the appropriate reference to save so that ConcreteA saves ObjectA and ConcreteB saves ObjectB, etc. > The polymorphic dispatch comes in when one used [Deconstructor] to > save objects from [ObjectB] and [ObjectC] classes. Instantiating > R1 correctly also means that one has to have the right flavor of > [Deconstructor] subclass object in hand when one invokes > Deconstructor.saveIt(). There are three tasks involved in saving an object, call it A. The first is obtaining the Deconstructor object. This requires knowledge of the concrete class of A and probably a dynamic cast or something similar to get it. The second is instantiating the relationship between A and the Deconstructor. If my diagram is correct above, it is a different relationship for each concrete class that A might have, so either this must be done at the same time the Deconstructor is being obtained or another dynamic cast is needed. The third is invoking saveIt() on the Deconstructor object, requiring nothing but the deconstructor object itself. This is where there is supposed to be polymorphism, but there cannot be real polymorphism if the concrete class of the object is still known, so this must happen in a different procedure from the first two tasks. There must be some saving procedure that is independent of the concrete classes involved and just performs general saving activities, one of which is invoking saveIt() on deconstructor objects that it gets from somewhere else. It is not clear how the separation of the first two tasks from the third task is expected to occur. Who knows the class of the deconstructor and who does not? Who is forced to deal with dynamic casts and who gets the benefit of polymorphism? It seems rather strange that one would go to the trouble of dynamically determining the class of the object to be saved and then not simply call saveIt() immediately. > IOW, subclassing [Deconstructor] and using it to save other > classes than [ObjectA] requires polymorphic dispatch of saveIt() > going to the right flavor of [Deconstructor]. Except that in the immediately previous sentence you implied that we must ensure that we have the unique correct flavor of Deconstructor if we are going to save the object we are trying to save. So what is being called polymorphic dispatch can only go to one place, a place we were forced to determine in advance. > That is essentially > the same as inheriting Serializer's saveIt() for the [ObjectA], > [ObjectB], and [ObjectC] classes. One invokes the same interface > but gets different implementations depending on which class one is > saving. It is nice in the Serializer pattern because we are not forced to know which class we are saving. It seems to defeat the purpose of polymorphism if we must to use a dynamic cast to get it. >> In fact, I think this entire technique for persistence could be >> simply described as the Serializer pattern, but with the >> implementations of the 'Serializable' interface removed from the >> actual Serializable class and into another object by using the >> Strategy pattern. > > Bingo. Give that man a kewpie doll. Actually, I was wrong! I think that saying this technique uses the Strategy pattern is misleading. The Strategy pattern is defined to include a reference from the Context object to the Strategy object, but that is lacking in this case, so technically it is not the Strategy pattern. In addition, it seems very tempting to put a reference from ObjectA to ConcreteA and if we do not make it clear that this is not actually the Strategy pattern, people will innocently put that reference in thinking that they are following this thread's advice.
From: Brendan Guild on 16 Sep 2006 21:04 H. S. Lahman wrote in news:4mWOg.537$HZ5.52(a)trndny08: > Responding to Guild... > > What I am pushing back on is what business reasons lead you to > expect that there will be a need for extensive maintenance? And > if there is such extensive maintenance, what would be the nature > of it? I see that you certainly know what you are talking about in game design as well as OOA/D. I believe this is my fundamental reason for needing to design the game in this manner and for expecting extensive maintenance: The entertainment value of the game will almost entirely come from the diversity of situations, creatures, and items that the player will encounter in an exploration and adventure setting. There will be no action and relatively little strategy, but the player gets better at the game by getting to know what could be called a jungle of entities. The jungle is what will require nearly all the maintenance and its size is expected to increase with each version. The entities are required to be diverse, and combined with an expanding set of entities it is nearly impossible to implement them with a single generic algorithm, though limited groups of entities might share one algorithm if possible. > I don't have any problems with this at this level of abstraction. > One suggestion, though, would be that the GoF State pattern is > probably more appropriate than the Strategy pattern. That's > because I would bet that decoupling entities will result in > complex protocols where multiple behaviors are invoked and where > there may be a need for internal role state carried between those > behaviors, which you mentioned above. It is true that the strategy object will likely carry state in many cases, but in the State pattern the state of the Context is represented by different objects and the relationship between the Context and the State is expected to be instantiated in different ways as the state of the Context changes. In the Strategy pattern, each pattern represents an algorithm for solving a problem and the relationship changes only if the algorithm that should be used changes. In my case, each strategy object represents an intended way for the entity to behave. Complex behaviour require that the object have implicit state, but the intention does not change, so the relationship is left intact. On the other hand, if there is ever a need to dynamically replace one strategy with another according to some state of the entity, there is nothing preventing me from doing that. It seems like the choice between State and Strategy is of very little consequence. > Another notion you might think about is viewing the collaborations > as complex activities that might warrant objects just to model the > collaboration itself. I suspect you are already heading this way > with your reference to an entity being a subsystem analogue. What > I suspect might happen is that the delegated strategies might > become complicated and need to be broken up themselves. Then a > Facade pattern around those objects could provide better > decoupling. I agree. I will definitely need to use a Facade between entities. Entities will need to give each other messages, such as when one entity uses another entity to smash yet another entity; the two passive entities would have to be informed. Also, modifying properties should be done through the Facade so that each entity is given a chance to react to each modification, instead of giving direct access of the property collection to other entities. > For <an extreme> example, a basic collaboration notion like one > entity attacking another could play out as an entire sword fight. > You could do that with individual interactions between the > entities for each thrust and parry or you could encapsulate the > fight itself with an infrastructure for AI strategy to sequence > moves, graphics control, and combat rules. The former would be a > necessity if the player controls the avatar's moves in the game > design. The latter, though, allows very generic interactions > between entities but requires a whole lot of infrastructure that > shouldn't be in a single god object. The benefit lies in the fact > that one can substitute various types of combat venues trivially > at the entity level by simply switching Facades. This seems very interesting! If you would like to expand upon it in more detail, I would be very pleased to discuss it.
From: H. S. Lahman on 17 Sep 2006 11:50
Responding to Guild... >>What I am pushing back on is what business reasons lead you to >>expect that there will be a need for extensive maintenance? And >>if there is such extensive maintenance, what would be the nature >>of it? > > > I see that you certainly know what you are talking about in game > design as well as OOA/D. Actually not. I used to do commercial simulations but not games. So my perspective is purely that of a player. > > I believe this is my fundamental reason for needing to design the > game in this manner and for expecting extensive maintenance: The > entertainment value of the game will almost entirely come from the > diversity of situations, creatures, and items that the player will > encounter in an exploration and adventure setting. There will be no > action and relatively little strategy, but the player gets better at > the game by getting to know what could be called a jungle of > entities. I can understand little strategy; the adventure games tend to be short of that. B-) But surely there must be some action. The player has to interact with the virtual world somehow or else it is just a fantasy travelogue. You mentioned Trolls clubbing things and using Jaws as a weapon. Are those all NPC activities that the player simply observes? What sort of things does the player get to know? How does knowing those things make the player better? I am pushing back on this because... > > The jungle is what will require nearly all the maintenance and its > size is expected to increase with each version. The entities are > required to be diverse, and combined with an expanding set of > entities it is nearly impossible to implement them with a single > generic algorithm, though limited groups of entities might share one > algorithm if possible. .... I am not convinced of that. For one thing a lot of games, especially shooters, depend strongly on graphics to provide variety. The monsters all work pretty much the same way but they have almost infinite variety in how they look. Thus some monster may have its outr? mouth in its abdomen, but it still uses it to eat and bite. It seems to me that the key is for the player to /perceive/ the entities as diverse. Graphics are one way to do that. A CIV-like scheme with fixed properties and a wide variety of values is another. Yet another is what I think of as the Chinese Menu system of selection (one dish from Column A, one from Column B, etc.). For example, it is common in defect tracking systems to classify defects with multiple different, fairly orthogonal characteristics (e.g., severity, location, and nature) that play together. When classifying the defect one selects one option from each characteristic. Each characteristic has a few choices. So if each characteristic only has 5 choices, one still have 125 unique choices for describing a particular defect. Thus for a combat system one might have one characteristic to describe attack mode (hurling objects, clubbing, biting, rendering limb from limb, etc.), another to describe primary agility advantage (quickness, speed, coordination), and yet another to describe primary physical advantage (strength, size, clock-stopping ugliness, whatever). And that's just for a combat system. Do that for other contexts, expand on it with a primary/secondary/tertiary system to allow multiple characteristics, and employ some fancy graphics and it seems to me you could provide a suite of entities that were perceived to be of almost infinite variety using a fixed set of characteristics. Now for that to work the characteristics need to play together. So in this example you would need a combat system that would factor them all into the combat. So it is a nontrivial game design problem to identify that fixed set of characteristics AND a combat system that works well. (Clearly I am winging it in the example and the characteristics would need a lot more thought.) But once you do that, I think that will be largely transparent to the player. Thus I can look at the CIV series and the configuration files and say, "Aha! I know how they are doing that!". That's because I have 20/20 hindsight because I spent a decade doing simulations so I have a pretty good feeling for the techniques used. But I doubt that the average player is going to look at the variety of units, buildings, etc. and recognize that there is actually a very simple underlying set of characteristics providing that variety. >>I don't have any problems with this at this level of abstraction. >>One suggestion, though, would be that the GoF State pattern is >>probably more appropriate than the Strategy pattern. That's >>because I would bet that decoupling entities will result in >>complex protocols where multiple behaviors are invoked and where >>there may be a need for internal role state carried between those >>behaviors, which you mentioned above. > > > It is true that the strategy object will likely carry state in many > cases, but in the State pattern the state of the Context is > represented by different objects and the relationship between the > Context and the State is expected to be instantiated in different > ways as the state of the Context changes. Yes, that is exactly what happens in role migration. Having a single [Context] object switch roles based on context at run time is what the GoF envisioned. But that is essentially the same as having different objects of the [Context] set each assigned a different role once at run time. The "context" is then just which Context object one has in hand. > In the Strategy pattern, each pattern represents an algorithm for > solving a problem and the relationship changes only if the algorithm > that should be used changes. You could also use Strategy to have different objects in the [Context] set always use the same algorithm that was different than the one other objects in the set used. Essentially Strategy and State are twins; the difference lies in the number of behaviors involved and whether internal state is required. [FWIW, I think State was misnamed; Role would have been much better. State suggests using it constructing state machines and it really isn't the best way to do that. Though there are code generation tools on the market that do exactl |