1

I have a table-like feature set in a game. What it means is I have 10 types of battle, and 10 types of features related to battles, like hints, start/end logic, rewarding/spending resources, statistic etcetera. For every type of battle each feature has it's own unique conditions and outcomes, resulting in total of 100 individual logic pieces. I'm unable to come to any adequate arranging pattern for this logic. And I need this pattern for team of developers to follow, otherwise there is complete chaos. What's really important, these logic pieces should be working somewhat independent in several different module-based projects, where each project can or cannot include some of features and battle types. And for the cherry on top: not all battle types should have implemented a logic for every feature.

1. Initially I had a swicth-case statement in every feature for each type of battle.

pros:

  • intuitive, easy to read (Clarity+)
  • quick to modify (Maintainability+)
  • every logic bit of a feature in one place (Clarity++)

cons:

  • all battle types listed in one place (Cohesion++)
  • necessity to modify code in every feature in case of adding new battle type (Extensibility--)
  • no code pattern, resulting in freedom to add anything anywhere (Maintainability--)

2. For my next iteration I've decided to mirror that scheme and list all feature logic bits inside controllers dedicated for each battle type. I've added one interface covering all features. It's somewhat based on Template-method pattern.

pros:

  • everything considering one type of battle in one place (Clarity++)
  • adding new type of battle is easy, just need to implement one interface (Extensibility++)
  • adding new feature is easy, just add new method to interface and be forced by compiler to implement it everywhere (Extensibility++)

cons:

  • one class contains too much different logic of different features. (Cohesion+++)
  • it's not clear where such controller should be located, considering module-based system of projects (???)

3. Now I'm planning to create a "table" of classes, each individual controller for each feature-battle crossing. For each feature there should be an interface, and for each battle type an implementation of such interface. Accessing such implementation would be through a special manager.

FeatureManager.Get<ISomeFeature>(BattleType.SomeBattle).DoLogic();

pros:

  • full abstraction. (Extensibility+++ Clarity-)
  • implementations can be placed inside module of a feature (Clarity+)

cons:

  • huge amount of tiny classes (Maintainability- Clarity-)
  • No compile errors in case of missing implementations. Need to develop new observer, to throw errors in case of some implementation missing, or existing when not expected, resulting in necessity to have small database with all feature settings. (Complexity++ Extensibility-)
  • nesseity to develop a special manager for accessing logic. All logic bit must be registered to it's battle type by mentioning ID from enum BattleType. (Complexity+)

None of these solutions seems good enough. So, what's the best course of action here? Thanks Internet!

Edit: here is a diagram for out projects and how modules are included diagram

Edit2: here is an example of that feature-set table. example table

10
  • Many large-scale games ended up implementing a scripting system, where the high level design (game design) can be done by editing a text-based script language, without having to rewrite the low-level implementation code. – rwong Nov 27 '20 at 9:30
  • 1
    All of your approaches are good ones at different stages of software development (i.e. prototyping). As the complexity of game design (note: game design is not software development) increases, there is an increasing need to decouple the complexity of game design from the complexity of software development. – rwong Nov 27 '20 at 9:33
  • 1
  • // one class contains too much different logic // Have you tried to extract some of those logic into utility functions and helper classes, in order to keep the high-level code tidy and easy to read? – rwong Nov 27 '20 at 9:48
  • Command pattern (link: Wikipedia) – rwong Nov 27 '20 at 9:49
1

Perhaps a shift of perspective can help you.

You seem to describe all feature implementations as "the so-and-so feature of battle type such-and-such". Is there some inherent concept about those features, that you have not abstracted yet?

For example, let's say your battles types have a ground feature that can modify movement speed. For instance in forests, troops move 50% slower.

class BattleType:
  def __init__(self):
    self.ground = GroundFeature()

  def modify_troop_speed(self, speed):
    return self.ground.modify_troop_speed()

class GroundFeature:
  def modify_troop_speed(self, speed):
    return speed

class ForestBattleType(BattleType):
  def __init__(self):
    self.ground = ForestGroundFeature()

class ForestGroundFeature(GroundFeature):
  def modify_troop_speed(self, speed):
    return speed * 0.5

Now imagine we rename ForestGroundFeature by SlowingGroundFeature and add a bunch of others. Now you can compose your ForestBattleType from features that have meaning of their own:

class ForestBattleType(BattleType):
  def __init__(self):
    self.ground = SlowingGroundFeature(0.5)
    self.visibility = DenseVegetationVisibilityFeature(density=2.5)
    self.random_events = DinosaurAttackRandomEventsFeature()

Pros:

  • Linear instead of quadratic amount of functions needed
  • Probably less code duplication, and more code reuse since features probably do similar things

Cons:

  • Good abstractions are hard to find. The system may be more complex to code now, but should be easier to understand later.
  • Using a feature palette is less flexible in the short term than free coding for every type/feature combination. You may need a way to combine features or just move to a more complex BattleType model than your 10-feature-slot model. It may end up looking like a domain-specific language, as @rwong commented.
  • I don't have a suggestion on how to compile-check this, but it may not be necessary since the BattleType implementations are easier to understand now.

(Answer to comment)

I took a look at your diagrams, Pavel, and I can't shake the feeling, that the table metaphor isn't the most helpful. Just having the grid fields makes people want to fill them in, even if nothing fits very well.

When I thought about the "feature palette" metaphor, I had more of a layer system in mind: Features and Battles as Layers

I think, this promotes a more manageable code base. I'd start out with a simple scenario and add one implementation for each layer. Sometimes a single, well-generalized implementation may be good enough, others will need branching out. (Side-note: what the layers should optimally be is probably a very difficult decision. You seem to have thought about which parts of the logic need to be adjustable, which is good.) Saying from the get go, "ok, we'll need 10 implementations for 10 scenarios each" seems like an invitation for trouble. Whenever your architecture slightly changes, you will have to modify at least 10 places. As more Battle types or Features get added, this gets harder and harder.

Take my opinion with a grain of salt, though, I'm only three years in the industry ;)

2
  • Thanks @TheHowlingHoaschd! Yes, some features, like hints can be set by using a config file with some settings, and some features are using common API where possible. But that is not the case for majority of features, like server commands, validation, specific graphics or enemies. I've added an exemplary table of features to my question, to clarify that. – Pavel Oliferovskiy Nov 27 '20 at 17:34
  • I think what you suggest with layers is closer to my first solution, with all it's cons. Imagine I need to add a new battle type - I'm going to search through all these layers manually and decide where I need to add what new logic. So this leads to lack of any pattern, which is making it increasingly harder for anyone to understand and maintain. – Pavel Oliferovskiy Dec 1 '20 at 8:30
1

You seem to have skipped a step in analyzing your requirements, because you're not leveraging the reusable parts of your code. Almost every column in your table hints at an interface/abstraction that can be made. I'll use a few examples from your columns in this answer.

Entry fee implies the need for an entry fee calculator. So what you do is you make a specific interface (IEntryFeeCalculator), and write your battle classes so that they can handle any IEntryFeeCalculator. For example:

public interface IEntryFeeCalculator
{
    int GetEntryFeeForPlayer(Player p);
}

public class PlayerLevelEntryFeeCalculator : IEntryFeeCalculator
{
    private readonly int _goldPerLevel;

    public PlayerLevelEntryFeeCalculator(int goldPerLevel)
    {
        _goldPerLevel = goldPerLevel;
    }

    public int GetEntryFeeForPlayer(Player p)
    {
        return p.Level * _goldPerLevel;
    }
}

I could come up with hundreds of different calculators. One that spews out a random number. One that calculates the price based on whether the player has bought the premium game license. One that calculates the price based on the day of the week. And so on...

But in all cases, the battle class can just expect to handle any entry fee calculator:

public class Battle
{
    private readonly IEntryFeeCalculator _entryFeeCalculator;

    public Battle(IEntryFeeCalculator entryFeeCalculator)
    {
        _entryFeeCalculator = entryFeeCalculator;
    }

    public bool CanPlayerRegister(Player p)
    {
        var entryFee = _entryFeeCalculator.GetEntryFeeForPlayer(p);
        
        // Player can join if he has the coin
        return p.Money >= entryFee;
    }
}

I hope the general outset is clear here. We defined a reusable behavior, i.e. calculating the entry fee, we created an interface that all our varied calculators will implement so that they can be used interchangeably, and then we make sure that our battle class only uses that reusable interface.

For the other examples, I'm just going to show you an example of what such an interface would look like. The implementations are up to you.

Availability

public interface IBattleAvailabilityEvaluator
{
    bool IsAvailable(Battle b);
}

Environment

public interface IBattleEnvironment
{
    void RenderGraphics();
}

Rewards

public interface IBattleRewardGenerator
{
    IEnumerable<Reward> GetRewardsForWin(Battle b, Player winner);
    IEnumerable<Reward> GetRewardsForLoss(Battle b, Player loser);
}

The others are similarly possible, though much depends on the precise details of how these things are implemented. I made a reasonable guess at the needed input parameters, but this of course hinges on your specific requirements as well.


So now we've got a bunch of calculators/generators/... and their interfaces, and we have the Battle class which works with these interfaces. So how do we put it all together?

Well, we could use a factory pattern to generate our battles with the correct dependencies:

public class BattleFactory
{
    public Battle GenerateStoryBattle(Player player)
    {
        return new Battle(
            new StoryNotCompleteAvailabilityEvaluator(), // IBattleAvailabilityEvaluator
            new StoryRewardGenerator(),                  // IBattleRewardGenerator,
            new StoryLevelEnvironment(player),           // IBattleEnvironment
            new PlayerLevelEntryFeeCalculator(5),        // IEntryFeeCalculator
            "Try gain better equipment"                  // Hint on loss
        );
    }

    public Battle GenerateArenaBattle(Player player, Player opponent)
    {
        return new Battle(
            new PlayerAvailabilityEvaluator(opponent),    // IBattleAvailabilityEvaluator
            new ArenaRewardGenerator(),                   // IBattleRewardGenerator,
            new ArenaLevelEnvironment(),                  // IBattleEnvironment
            new PlayerProgressEntryFeeCalculator(player), // IEntryFeeCalculator
            "Try go to encounter"                         // Hint on loss
        );
    }
}

I skipped the other dependencies, but the principle remains the same. First, you develop all your needed components, and then the factory assembles your battles using the components.

This factory is, in essence, using your table to put it all together. But before it can do so, you need to develop all of the features in that table (using the columns as a guideline for abstractable behavior).

Also, notice that the hint is just being passed as a string. This is to show that not every dependency has to be a class of its own. Sometimes it might just be a value that you pass into the battle class, without it needing to be dynamically generated.

7
  • Thanks for the answer! But I think you answered a bit of a different question. First of all I've already explored the idea for an interface for each feature in my third solution. The problem with reusable code is not relevant to my question, since I can just call any common reusable logic from individual implementation for each logic. – Pavel Oliferovskiy Dec 1 '20 at 8:16
  • The main problem here, as you said, is how do we put it all together? And you are suggesting hardcoding implementations into battle class fabrics, which is inconsistent with my requirement of not all features being included in each project. Meaning I'm going to need to create fabrics again for each new project, and that is exactly what I'm trying to avoid. – Pavel Oliferovskiy Dec 1 '20 at 8:16
  • How about this: define a separate factory for each project, and determine based on config files, which one to use. I like @Flater's idea of not even making subclasses of Battle. The assembly details can be specified by a factory method, and those can be changed based on project. – TheHowlingHoaschd Dec 1 '20 at 9:25
  • @PavelOliferovskiy: I strongly disagree with many of your conclusions on what is a good/bad approach. You specifically list a lack of clarity as a negative when supposedly using smaller classes - quite the opposite is true (up to a reasonable point of course, as with all things). Many of your conclusions are either wrong, misinterpreted, or rely on the assumption that there is only one possible implementation (i.e. the one you could think of) and completely ignore the possibility of any other implementation which might not entail the highlighted issue (e.g. your "battle enum" con in #3) – Flater Dec 1 '20 at 9:32
  • @PavelOliferovskiy: How you tie it together is up to you. I showed you the simplest way, which assumes that each battle type has an effectively hardcoded composition. You can use a config file for this if it's more dynamic. You could use a database. But the question is what your requirements are, and how complex you need your configuration to be. There's no point implementing a config database if the requirements are effectively hardcoded. This brings me back to my initial point, you've not completed (or fully communicated) your analysis and requirements. – Flater Dec 1 '20 at 9:34

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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