7 ms·
> The vast majority of essential code is not operating on just one object – it is actually implementing cross-cutting concerns. Example: when class Player hits(
by jryan49 8y ago
> The vast majority of essential code is not operating on just one object – it is actually implementing cross-cutting concerns. Example: when class Player hits() a class Monster, where exactly do we modify data? Monster's hp has to decrease by Player's attackPower, Player's xps increase by Monster's level if Monster got killed. Does it happen in Player.hits(Monster m) or Monster.isHitBy(Player p). What if there's a class Weapon involved? Do we pass it as an argument to isHitBy or does Player has a currentWeapon() getter?
Not saying this is the right way to do things but, if you're actually going to go 100% OO on something:
Player.hits(Monster) returns Hit
Hit takes a Player and a Monster in constructor, and has with(Weapon)
You end up with:
Player.hits(Monster).with(Weapon)
Then Hit reaches into said objects and deals with changing the HP, and XP. You then have encapsulated the details in the actual Hit itself, which seems correct.
It does read kind of nicely IMO...
- deleted 8y ago[deleted]
- jstimpfle 8y ago> It does read kind of nicely IMO... And this one line is why you're willing to setup all these crap boilerplate supporter classes and take a huge hit in runtime performance?
- jryan49 8y agoA lot of languages are made for this kind of interaction and can inline stuff hopefully, but that's the job for the VM or compiler. I didn't say it was the "right" way to do it. Also programming is the art of tradeoffs. If this does actually give bad performance, maybe it's a trade off the programmer is willing to make. Just like the article started off with "I don't think there's a silver bullet". OOP is a tool just like any other with pluses, and minuses. Also the Hit class may be a Singleton and only instantiated once.
- jcelerier 8y ago> And this one line is why you're willing to setup all these crap boilerplate supporter classes and take a huge hit in runtime performance? at least in C++, why would they ? it'll all gets inlined and you can do it so that there isn't any memory allocations if you know all of your cases at compile time (of course most of the time you want to be able to add new weapons, behaviours, etc, at runtime but in game engines you generally use scripting languages for this anyways).
- jstimpfle 8y agoIt's nice that some C++ compilers can optimize this out. It means that it only costs maintainer cycles (less straightforward invocation), and compiler cycles. As every C++ programmer knows, there is an unlimited amount of them.
- Novashi 8y ago>And this one line is why you're willing to setup all these crap boilerplate supporter classes and take a huge hit in runtime performance? Hits don't usually happen that often in games relative to everything else the engine is doing unless you're writing something like MMO server code that processes hits from thousands players. Yours is a classic case of over-optimizing, I think.
- jstimpfle 8y agoOver-optimizing as in, write the one obvious line instead of 15 lines of crap that are also slower?
- Novashi 8y agoUntil you get a decent combat system in and now your hit has to go through 30+ calculations. It's almost never as simple as "monster.HP - damage". Besides, if we really want to be snarky, why haven't you wrote it in assembly for performance?
- jstimpfle 8y agoNo, I'm not talking about one arithmetic CPU operation. I'm talking about simple, procedural hit(monster, weapon, damage). Why not in assembly? Because it's not worth the effort. You won't notice any difference in speed. Be pragmatic.
- Novashi 8y ago>I'm talking about simple, procedural hit(monster, weapon, damage). The moment your game needs to know anything else about that hit event, you're going to start creating side effects from that procedure and that QUICKLY snowballs into spaghetti code. Not to mention, just from the signature, that's going to be an absolutely huge procedure. >Because it's not worth the effort. You won't notice any difference in speed. Be pragmatic. That's the exact same case I'm making. A hit object every so often will have no impact on performance (certainly not a huge impact) like you originally said and you gain developer productivity.
- matthewmacleod 8y agoI'm pretty confused by how you perceive this. I don't see "crap boilerplate supporter classes" – I see objects that encapsulate events and actors for all the usual OOP reasons. I wonder if some of this disagreement is down to the way that different people abstract this problem in their heads.
- jstimpfle 8y ago// player.hits(monster).with(weapon) add to class Player { // XXX: Bad coupling Player -> Monster // XXX: Bad coupling Player -> PlayerMonsterHit method hits(Monster monster) { return PlayerMonsterHit(player, monster) } } class PlayerMonsterHit { member Player player; member Monster monster; construct PlayerMonsterHit(Player player, Monster monster) { this.player = player; this.monster = monster; } method with(Weapon weapon) { doTheActualFrigginHit(this.player, this.monster, weapon); // this line is all we REALLY need } } I guess 15 lines (that do NOTHING) was not an overestimation.
- matthewmacleod 8y agoThey don't "do nothing" - they define structure that can then be used to handle more implementation later. It's a ridiculous, contrived example, and you know that. Yes – this code is stupid if all you need to implement is a single line with the ability for a player to hit a monster. But that's never what you are implementing, is it?
- jstimpfle 8y agoIf you want to delay the hit, i.e. transactioning / buffering / delaying: This is sound engineering! But that part is not even included in above boilerplate. It would be: struct PlayerMonsterWeaponHit { Player player; Monster monster; Weapon weapon; } Array<PlayerMonsterWeaponHit> playerMonsterWeaponHits; doTheActualFrigginHit(Player player, Monster monster, Weapon weapon) { playerMonsterWeaponHits.emplace(player, monster, weapon); } In case it isn't obvious you can do that with either implementation, the above OOP boilerplate or the above imperative 1 line of code. > Yes – this code is stupid if all you need to implement is a single line No, it's stupid because it doesn't do anything. The data structures are only for temporaries that will never have any other use. In the end it's going to be PlayerMonsterWeaponHit. So the other classes are just stupid, and no amount of context will change that.
- crazygringo 8y agoGreat point... but doesn't it continue to prove the author's point that this is needlessly complex? It strikes me as bad design if something as simple as an action (hit) now needs to become it's own class that is instantiated merely to execute a single function and then have to delete itself. That just feels like insane overhead/boilerplate, no? The argument here is that it's far better to have player and monster be simple data structures, and a single function hit(player, monster, weapon).
- jryan49 8y agoThe Hit class may be a Singleton and only instantiated once. Creating a class is like 3 actual lines. Also at least in Java the Hit class can be an inner class of the Person class. It just feels like name spacing in that case. It is needless complex if the code is simple. This solution is more flexible that just a pure function. If you have different players who use different hit strategies how do you handle that with just a pure function? Lots of if statements in the function? Passing in lambdas in every case you call the function? Then you could curry the function and call it something different too and that works I guess. OOP shines when you have lots of different ways things can work in different combinations. OOP is a tool like other programming paradigms, you use it when it solves your problem better than the other ways.
- geofft 8y agoUltimately the answer is to use a language in which passing a function reference is as easy as writing a singleton that implements an interface and passing a reference to that. (Or easier—in any language with decent function-passing support, writing a function and referencing it is generally going to be easier because fewer things are happening.) OO makes sense if the different types of behavior map to object identities and inheritance trees, because it generates a chain of implicit if statements that capture this logic. If they don't—e.g., a hit changes behavior depending on whether the attacker is proficient in the weapon, rather than depending on the weapon class or attacker class itself—you're likely better off writing the if statements yourself.
- gameswithgo 8y ago
- arriu 8y agoThis does read nicely in most cases. There are minor issues when you venture off and decide that it would be cool if a monsters could damage your weapon. Something like Monster.hits(Weapon).with(Weapon). You'll start wondering if Weapon should inherit from the player base class. If not, you'll spend time wondering how to share the code to keep things dry. Obviously there are many solutions even within OOP, but one might consider looking to ECS to remove code duplication and decouple things. Lots of good examples in the game programming patterns book: http://gameprogrammingpatterns.com/component.html http://gameprogrammingpatterns.com/component.html
- jryan49 8y agoI think damage to the weapon could still be handled in the Hit class, because Hit has a reference to Player and Monster and Weapon. You could even create different types of Hit classes that could execute different kinds of Hit behavior like this. In that case it may be better to rename it to HitStrategy?
- alasdair_ 8y ago>You'll start wondering if Weapon should inherit from the player base class. Probably not. If you want to do that, they should both implement an interface (and perhaps share some code through composition) but code inheritance is likely not the right approach here.
- ActingMike 8y agoOOP is here to stay because it is the default model of how we see the world. You actually do not have to teach people to use this, just how to map it to the OOP Software Systems like Java/ C#. It was great while Moors law still lasted, and to write efficient software was becoming some strange quest for some formula one fields of software like games or other massive workload fields like OS-Wizzardry. Today, the massive workload is still growing, due to sloppy architectural habits and user demands, but the saviors of parallelization and new chip technologies have not come to rescue the sloppy-devs from the laws of leaky abstraction. Your code runs on a hardware system. And currently your code is running out of hardware. And with that OOP is running out of excuses. So you would write this. Then it would three months before the shipping be handed to some adult- and this adult will delete it. All of it. Its full of references, full of class-bloat which will push one another out of the cache. Even though the compiler will try to yank the worst out this mental crutch out of the code. Some references can not be dismissed deterministically, so they will be kept. Which makes it incredible slow. What replaces it will be a efficient structure, keeping all the chunks necessary for collision detection, in one very array like field. Over this a Algo traverses, acting with a function, which does not reference non-local stuff, detecting collisions and damage. Allowing not only for a few players- but a workload of players. It will look very ugly, like C, with inline assembler- ugly, and it will be ready to ship.
- naasking 8y ago> OOP is here to stay because it is the default model of how we see the world A common but false statement. Firstly, there is no standard definition of OOP, only loose sets of features that define various overlapping but disjoint paradigms that some subset of people call OO. So your statement is meaningless as there is no single OOP that we identify as how "people see the world". Secondly, even given any specific definition of OOP, it's also false. There are many programs that simply aren't suited to OO modelling, because you want to express things as the problem being solved actually requires, and this is often not OO. Sometimes pattern matching is best (compilers), sometimes reactive/event paradigm is best (servers, UIs), and neither of these are OO, as but two examples. And don't take my word for it, there are a few studies on novice programmers showing quite clearly that event-based temporal reactive primitives are what people find most intuitively natural, ie. when account balance <= O then do some action, which is a declarative reactive action that will execute when that condition becomes true. This is not an OO program in any sense.
- fenomas 8y ago> You end up with: > Player.hits(Monster).with(Weapon) I think games are a particularly pathological case for OOP, and as such probably not a good example for the article's case. But FWIW, the problem with what you describe (and with OOP for games generally) is that game logic tends to be way too polymorphic for code like that. That is, players don't just Hit() monsters, they also hit items, traps, breakable terrain, etc., and they get hit by monsters, by projectiles, maybe explosions, fall damage, etc. And the dilemma of doing all that in OOP is, you find yourself with 20 different things that can receive a Hit(), that have little in common otherwise. Some have hit points but others don't, some don't have an armor value, some need to receive knockback but others don't even have a physics body, etc. As a result, do you make Hit() accept 20 different types, with special cases for each? Or do you rejigger your class hierarchy so that those 20 classes all inherit from Hittable, etc? Either could work in this or that case, but neither's much fun. This is all why ECS (or other aspect-based approaches) are so popular for games - they let you define very general "Hit(src, tgt)" chunks of logic, that don't care what type each object is, but can easily query whether or not they have hit points or a physics body. But again, I think games are a pathological case here and none of this should necessarily be considered an argument against OOP generally.
- scarface74 8y agoI find the fluent style programming to be much more readable. But in OOP you would define an IHittable interface or use AOP like you suggested.
- fenomas 8y agoYou can define iHittable and get the code to compile, but it doesn't make the actual issue (polymorphism) any easier to tackle. I think eschewing inheritance in favor of aspects is definitely the way to go. (Of course, you can wrap everything in a fluent-style interface either way - that part is orthogonal to all the rest.)
- pas 8y agoECS https://en.wikipedia.org/wiki/Entity%E2%80%93component%E2%80%93system https://en.wikipedia.org/wiki/Entity%E2%80%93component%E2%80... for others like me who had no clue.
- mannykannot 8y ago"Then Hit reaches into said objects and deals with changing the HP, and XP." This is not essentially different from what happens in non-OO code. OO purists would probably object to this as a violation of encapsulation, and would insist on objects receiving a hit performing the update to their state themselves. Stepping back from this example and considering transactions in general, it is often the case that there are certain constraints to be observed, and in such cases, it is easier to verify the solution if it is done compactly in one function. In such cases, creating a class and instantiating it just to have that function would be pointlessly excessive obeisance to a principle, and making it a singleton would only underscore that point.
- iokevins 8y agoEric Lippert discusses this here (2015): https://ericlippert.com/2015/04/27/wizards-and-warriors-part-one/ https://ericlippert.com/2015/04/27/wizards-and-warriors-part...
- scarface74 8y agoFirst I had to have restraint to not downvote you for making me go down that very interesting 5 post rabbit hole.... Second, in one of the articles I found one of the best quotes I’ve ever heard about when not to use exceptions.... When you’re writing code in the IDE, it is correct code that is the exception!
- szastupov 8y ago> Player.hits(Monster).with(Weapon) The problem is that it's an artificial example, in reality you'll ave a lot of different monsters, weapons, and most importantly separate subsystems. I.e. physics processed in one pass, animation in another, same goes for custom logic and rendering. Modern game engines use component systems that put data first for the same reasons author described in the post.
- z3t4 8y agoit's a trap. you don't want to complex objects/classes. keep them simple!
- lerno 8y agoBeen exactly there with a game. Solution? Take a data centered approach: a command object (just a function really) updates HP, XP, triggers visual effects etc. Monster and Player classes are just thin wrappers around plain data structures. I tried the more ”OO”-approaches for all too long thinking it would lead me right. It doesn’t.