3

I'm learning to make games with OOP and there's something I don't understand. What I can see is that the more I add methods to a class, the less it becomes reusable and flexible. For example, if we have a Player class which we can make jump:

class Player {
  Vec2 position;
  Vec2 velocity;

  update() {
    velocity *= 0.9;
    position += velocity;
  }
}

This seems good, the calling code can call update in the main loop to update the player:

mainLoop() {
  player.update();
}

Now let's suppose that I have a "boss" that can grab the player. The update method doesn't do what we want now, what we would want is to directly set the position of the player without updating it physically.

Does this means that I have to modify the Player class to add a GRABBED state in which case the update method doesn't update the position?

enum State {
  DEFAULT,
  GRABBED,
}

class Player {
  Vec2 position;
  Vec2 velocity;
  State state = DEFAULT;

  grab() {
    state = GRABBED;
    // Now the calling code can update the position freely
  }

  update() {
    if (state == DEFAULT) {
      velocity *= 0.9;
      position += velocity;
    }
  }
}

Now the calling code can do:

mainLoop() {
  player.grab();
  player.position = boss.hand.position; // for example

  // The update still occurs at some point
  player.update();
}

Does this seem reasonable? I think it's strange to have to update the Player class to support doing something else with the player. We don't necessarily know beforehand what every possible uses of the player are.

Another idea I've seen is the component pattern (https://gameprogrammingpatterns.com/component.html), where we would have some "component" defining a behavior that we can change at runtime (like the Strategy pattern).

But all of this seem cumbersome and I don't understand what the benefits are compared to keeping the Player class to a minimum and letting the outside code do whatever they want:

class Player {
  Vec2 position;
  Vec2 velocity;
}

mainLoop() {
  if (isFree) {
    defaultPhysicsUpdate(player);
  } else if (grabbed) {
    player.position = boss.hand.position;
  }
}

defaultPhysicsUpdate(entity) {
  entity.velocity *= 0.9;
  entity.position += entity.velocity;
}

This code seems more flexible and simple to me, so why would we want to put everything in the player class?

1
  • You mention the Strategy pattern. That seems like the right use to me. If you want to change the algorithm for moving a player at runtime, Strategy pattern is the way to go. You can have a DefaultMovementStrategy and a BossGrappedStrategy. Mar 1, 2022 at 10:20

5 Answers 5

2

I think you are on the right track. You want the Player object to allow autonomous movement, but without putting too much knowledge about the "outer game mechanics" into it. A "grabbed" state inside the player does not fit to this design, and you are perfectly right: introducing such attributes which are only useful for exactly one use case will make the class less reusable and flexible.

One can solve this by developing a clear idea what fits well into a certain abstraction, and what not. That's what we call the responsibility of a class. In this case, you want the Player to be an abstraction for a physical game item - hence it makes sense let the player know only it's own physical properties (like velocity and position), and maybe some meta information (like a name or id), but not many more.

Since the game will allow changing velocity and position of a player "from outside", you need to give it getters and setters for both. Making a boss "grab" a player just sets the player's velocity to zero - no need to introduce an extra state attribute for this (of course, this comes for the price of having the player "forgetting" its former velocity). When the boss lets the player go, the game mechanics has to set a new velocity for the player - but which velocity that will be is part of the game rules, nothing a player object should care for.

Still you can (and should) implement all methods inside the Player class which can be implemented reasonably by making use of player's internal attributes, like your update method. I would consider to use a more expressive name for it, like moveAutomatically. Such a method fits perfectly into your player abstraction of a "physical item", and placing it there helps to keep your program well organized.

When your program grows, and you add more and more use cases to it, you will several times have to make a decision whether the Playerclass should be extended, or whether a certain functionality should be kept out of the class. When you stick to the responsibility you envisioned, this usually leads to some convergence - for the first use cases you implement, the class has to be extended and refactored, but at some time in the future, the class becomes "feature complete" and stable. Then new use cases can be implemented by simply reusing the Player's methods and attributes which are already there.

1

If you update positions of your characters iteratively based on their properties you could add some extra conditions that are outside the scope of the object itself. This would nicely reflect the powerless nature of a grabbed character.

Say some boss grabs your player, rather than flagging the player as grabbed you could have it removed from the list of characters to be updated on each iteration.

The boss chases a character and when his position is close enough to the player's, he could raise an event to the player manager object to request that player to be removed from the pool of free moving players and handed to him. In the same way he could release the player, having it re-added to the pool of free players.

Grabbed is an external condition which I would say should not be a property of player.

It may still be useful for a player to know its grabbed state if he could do something about it like struggle himself free or deploy some defense mechanism. But as long as that is not the case there is now reason for him to know, he would just be taken out of the game.

1

"Why would we want to put everything in the player class?"

We wouldn't (but in OOP, we generally wouldn't want the class to contain no methods either). Classes should be small and focused. Based around "one thing" that they do well (whatever "one thing" means in the context of your game). This is either some specific, very concrete functionality, or some logic that orchestrates other objects.

"What the benefits are compared to keeping the Player class to a minimum and letting the outside code do whatever they want. This code seems more flexible and simple to me"

At first glance it may seem more flexible, but here's the flip side of the coin: that outside code needs some kind of organization as well - otherwise, as you add more and more code, you'll have the same problem as putting everything inside the Player class, except it'll be in the code for your game logic. You won't be able to easily reuse parts, and it'll tend to become increasingly tangled and complicated over time. So, it should also be broken down into small, focused classes/functions.

You'll also likely want to introduce some kind of separation between code that implements the underlying engine functionality, and code that's more about the high-level game(play) logic (maybe not immediately, but as your codebase grows). Eventually, you might want to let a scripting language handle parts of that.

Now, figuring out how to do all that is not necessarily easy, there will be a lot of trial and error (even if you read everything there is to read about game design), and sometimes there will be other concerns (like performance) that may dictate your decisions or constrain what you can do in practice.

Even if you're not using traditional OOP, but something like ECS (Entity-Component-System), you still have this separation of concerns - it's just expressed in a different way. In ECS, Components are essentially behaviorless data structures, but they are small and focused on a single thing (rather than "player", "enemy", "item", they represent concepts like "something with a position", "something with a rotation", "something with a bounding box", "something with a sprite", etc.), and they are reusable in the sense that you can combine them with other components to create different kinds of Entities. And then you have Systems which are, again, specific, narrowly focused behaviors, that operate on these components (or a certain combination of a number of different components).

So no matter how you cut it, you'll always have to watch out for code getting bloated and unwieldy, and at some point decide to restructure it before it gets too bloated and too unwieldy (note: this can happen sooner than you expect). Just don't try and design everything up front - do a little bit of design, keep it simple, then course correct along the way, as you sort of learn more about what your game needs and how it should all actually fit together. Otherwise, you'll create overspecified designs based on wrong initial assumptions, and you won't be able to easily backtrack from that.

1

I don't understand what the benefits are compared to keeping the Player class to a minimum and letting the outside code do whatever they want

You can do that. The player becomes a mindless data structure. The cost is now everything that touches a player must know that structure and manage it's data. This doesn't minimize player logic. It just scatters it to everything that knows about players.

The alternative is encapsulation. Rather then know anything about a players internal state you just send it messages that tell it what you're doing to it. The player decides what to do about being grabbed. Yes that means you have to create a method that can be called to send that message. Nothing in life is free.

But done this way you can polymorphically change what a player does when grabbed as per the strategy pattern. This way nothing else has to know or manage the players response to getting grabbed.

Now say some behavior or state of the boss must be expressed. Now the grab is about more than what the player knows. The player must learn something here. How?

enter image description here

mjolnir.grab(worthiness: beers_today < 2);

You tell it. That's how.

1

OOP is a way of thinking about things. It helps you break down a complex problem in smaller, more easily digestible chunks. More interestingly, OOP uses a model that humans tend to already innately think in: individual objects, each with their own qualities (properties/fields) and abilities (methods).

Before we know what code to write, we first have to analyze what it is that we want. In your design phase; you've glossed over an important part of what you need your game to be able to do, because I can tell you've not designed your code to account for it.

Forget about the code for now. Let's talk about your game and how you like it to work.

Now let's suppose that I have a "boss" that can grab the player.

This immediately suggests that the player is not the sole arbiter of their movement. Outside forces are able to interrupt a player's abilities to move or jump around.

I think it's strange to have to update the Player class to support doing something else with the player.

If you think about it, that is precisely what you want the boss (or their hand) to be able to do: change the player's state.

Does this means that I have to modify the Player class to add a GRABBED state in which case the update method doesn't update the position?

That is one way to do it. However, I would expect that your game would have more than one way to override the player's behavior. "Grabbed" is a very specific term that refers to the current scenario, but you might have other reasons to implement the same kind of overriding behavior for which "grabbed" is not the right name, so you'll likely want a more generic sounding name for this.

This is not a given. You could use specific states such as "grabbed", "pushed", ... but then you have to really analyze what these states mean and how they affect your player. In the interest of simplicity, I'm going to keep this answer a bit more generic than that.

The "boss grab hand" feature leads us to specific requirements:

  • An outside actor should be able to inhibit the player's own free movement
  • An outside actor should be able to move the player around

This leads us to design our class to account for these things. I'm skipping the specific movement logic here because the answer applies regardless of how you implement the movement logic.

I'm a C# dev and, but the syntax should be clear enough for your case. I'm not 100% which language you're using.

public class Player
{
    private bool canMove = true; // By default, player can move

    public Position Position { get; private set; }

    // Outside actors can tell the player that they're (not) able to move
    public void CanMove(bool value)
    {
        this.canMove = value;
    }

    // Logic involving the player's own voluntary movement
    public void Move()
    {
        if(this.canMove)
        {
            // Movement logic which updates the Position property
        }
    }

    // Outside actors can move the player around.
    public void SetPosition(Position p)
    {
        this.Position = p;
    }
}

This is a basic implementation of the requirements that followed from the "boss grab hand" feature you need. If you want, this could be developed further:

  • SetPosition could require the outside actor (boss object) as a parameter, and only allow its position to be set if the actor has the required Strength state to do so (e.g. actor.Strength == this.Strength || this.State == PlayerState.Asleep, as a crude example)
  • A similar check could be implemented for CanMove(bool), whereby an attempt to render the player unable to move is not guaranteed.
  • You could implement some sort of skill check whereby the player rolls a die to try and wriggle free and actually still manage to move even though they were set to canMove = false.

These are just examples of how you can further flesh out this logic; but it requires specific design decisions on how you want your game to function, and this is beyond the current question.

Another idea I've seen is the component pattern (https://gameprogrammingpatterns.com/component.html), where we would have some "component" defining a behavior that we can change at runtime (like the Strategy pattern).

These are all valid ideas, but they are more complex to implement. A lot of this hinges on your specific requirements and how you intend your overall game to work.

For a sufficiently complex game; things like ECS are really helpful to separate individual gameplay features and keep them dynamically adjustable. However, for a fairly trivial game, this would be overkill.

But all of this seem cumbersome and I don't understand what the benefits are compared to keeping the Player class to a minimum and letting the outside code do whatever they want

There's certainly a style of game where letting the outside code do whatever they want is a valid implementation. I'm not sure if you have experience with Dungeons and Dragons; but in this game the GM (a person) is the sole arbiter of what happens.

The GM is honorably bound by gameplay rules that the players expect the GM to follow, but the GM effectively has the sole authority to tell a player where they move to, or whether their action did(n't) succeed, or how much damage they took, or ... If the GM says it happens, it happens. They are, for all intents and purpose, the god of their game's state.

In a DnD-style game, all your game objects would essentially boil down to state containers, and the GM would be free to set any property/field to whatever value they desired. You wouldn't even need to validate any of these actions, assuming that the GM is a trusted source who is certain that they only perform the correct actions.

However, at the opposite end of the spectrum, consider a multiplayer game. If all game objects can let outsiders do with them as they want, it would be trivial for a player to cheat by messing with their opponent or changing their own stats to gain an unfair advantage.

This is a matter of how you structure your game logic, and this is a discussion much bigger than the question you asked here.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.