TBS game, any advice for a beginner?

I shall begin with a disclaimer: I am just an amateur programmer, and little knowledgeable, so I don't really know what I'm doing. I would need some general advice before going on with a project of mine, to avoid wasting time with an ill-designd approach.

I began writing a turn-based strategy (TBS) game. I thought it would be a good idea to split the project in two: a generic library providing some tools to implement a generic TBS, and the actual game using the library.

The library defines traits, which are used as trait objects. For example:

trait Class {}

struct Unit {
   class: Box<Class>,
}

impl Unit {
    pub fn new(class: Box<Class>) -> Unit {
        Unit {
            class,
        }
    }
}

The user of the library is then expected to create its own types implementing Class:

struct Warrior {}
impl Class for Warrior {}
let warrior = Unit::new(Warrior{})

Then, to inspect Units' class, I guess I will need to use Any and downcast. To make things easier, I'm using the mopa crate, so I redefine Class as:

#[macro_use]
extern crate mopa;

trait Class: mopa::Any {}
mopafy!(Class);

My main doubs are:

  • Is it a good idea to implement a separate, generic library?
  • Is using trait objects and Any a good idea? I read around that Any should be used only in very particular situations, but I don't see how I could avoid using it.
  • Are there other issues I am not aware of?
1 Like

Typically, a library would be for some generic or independent functionality, like detecting input or rendering shapes for example. For something that builds a TBS, that would be more of a framework or engine, which depends on those. Then, the game itself would be a specific configuration in it, like filling in the rules per player turn, etc. Maybe that's what you had in mind :smiley: It seems like a great project to get started with. Your head is in the right place it seems. There are probably multiple ways you could code this, so as long as it makes logical sense and you are happy with it, then all is good. Making generic libraries and code is a good practice. There are probably better ways than downcasting since Rust traits can have common interfaces across types, and there wouldn't be too much type complexity to this project ideally, but it's not that bad if you know the right types when downcasting I'd say. Rust is very capable of different ways, but doing things the Rust way is a new territory for programming practices, and you will have to work within its memory safety strictness. Should be ideal for your project since I see it having very hard-coded states and routines. Since you want to break that into a library/framework, I would work on how you want that system to work from the high-level, how it could be used by others, maybe using a typical pattern for such, and work out the details and types accordingly. That's about all the advice I could offer, but good luck! I'd like to see it when it's playable.

1 Like

It might be a little off topic but I would suggest reading game programming patterns. It's the easiest introductory material for the topic I had read (my friend who works at an AAA studio told me its light years from being PRO but its nice to get your feet wet. So high praise :wink: ).

Mind that the code samples are written in CPP but the author has many years of pro gamedev experience and an excellent writing style and much of the concepts should be easily translated.

4 Likes

Thanks for the suggestions and the encouragement, I will need both!

That looks what I am trying to accomplish. Is a Rust library crate the best way to do that? I don't need a binary crate, right?

The problem here is that I expect types implementing those traits to potentially do very different things, so I am not sure I can provide a common interface that suits all needs. I may change my mind on that, though, when I put those traits into use.

Thanks, that looks most helpful!

My plan is to publicly release the code on GitHub/GitLab (under a permissive licence like MIT) once it reaches alpha (if ever). It's not available yet because at the moment it's still more of an idea than something concrete. It will never be anything fancy, but I hope it will be fun to play.

You may want to look into the ggez lightweight 2D game framework. In addition, look into the concept of Entity-Component Systems.

2 Likes

Since this is getting far too vague, I'm posting a piece of code as an example. The rest of the structure I have at the moment is analogous:

/// Anything which can affect a `Character` (e.g. `FireDamage`)
pub trait IsCharacterEffect {}
pub type CharacterEffect = Box<IsCharacterEffect>;

/// Anything a `Character` can do (e.g. `CastFireball`)
pub trait IsCharacterAction {}
pub type CharacterAction = Box<IsCharacterAction>;

/// `Character` is the trait characterizing any kind of character,
/// abstracted from the specific instance of `Game`.
pub trait IsCharacter {
    /// Converts an action of the character into the list of `GameEffects` the action produces
    /// (e.g. `CastFireball` may inflict `FireDamage` to the target `Unit`)
    fn effects(&self, _character_action: CharacterAction) -> Vec<GameEffect> {
        Vec::new()
    }
    /// Applies a `CharacterEffect` on the character
    /// (e.g. `FireDamage` may reduce health-points)
    fn affect(&mut self, _effect: CharacterEffect) -> Result<()> {
        Ok(())
    }
}
pub type Character = Box<IsCharacter>;

pub type UnitEffect = CharacterEffect;
pub type UnitAction = CharacterAction;

/// A `Unit` is a container for a `Character`
/// which includes the data to place that `Character` into the `Game`.
pub struct Unit {
    // Uniquely identifies the `Unit` into its `Team`
    unit_id: ID,
    // Identifies the `Team` the `Unit` belongs to.
    team_id: ID,
    // The `Character` of the unit.
    character: Character,
}

impl Unit {
    pub fn new(unit_id: ID, team_id: ID, character: Character) -> Unit {
        Unit {
            unit_id,
            team_id,
            character,
        }
    }

    pub fn effects(&self, character_action: CharacterAction) -> Vec<GameEffect> {
        self.character.effects(character_action)
    }

    pub fn affect(&mut self, character_effect: CharacterEffect) -> Result<()> {
        self.character.affect(character_effect)
    }

}

My idea is that a user of the library may define its own types. E.g., CastFireball would implement IsCharacterAction.

I incourred in various kinds of akward situations. For example, all of the traits are encapsulated into boxes. Though, this requires to use Any and downcast to inspect those types. Moreover, I have to box things which I thought could just be generics, such as the character field in Unit, because that way I could not have a Vec<Unit<IsCharacterKind>> field in a struct.

Working seems to be working, but the code feels akward at times. Maybe my approach is too abstract and I should refactor it in a more concrete fashion…

Personally, I would recommend using enums rather than traits to start out. They are more explicit, since you list every variant, and the compiler can help you with ensuring that each case is handled. It doesn't make things as separate or as extensible, but does make it simpler to create special interactions (eg, cleric does double damage to undead, fires immediately destroy wood golems).

New rustaceans often seem to reach for traits and Any when a simple enum might be easier. I'd also hold off on the library until you've gotten something working. I look forward to hearing about your progress!

2 Likes

I found the suggestion to use enums rather than traits elsewhere too. The problem is that that's possible only if it's known which enums variants will be needed. For example, I should already know that Class has to be one of Warrior, Ranger or Mage.

The library I am writing abstracts over this, so it is not possible to know which, and how many, different kind of Classes there will be. On the other hand, abstracting just a little seems to make little sense to me…

So, as far as I can see, there are only two possible approaches, and those are completely distinct: either I explicitely define the components of my game with enums, or I use a library providing traits.

I can see some pros and cons for each of the two possibilities, and I can't make my mind up. Maybe I should write a (relatively quick) first iteration of the game using hard-coded enums and no dedicated library. Then, if the library seems promising enough, I can rewrite the game using it. It will take long, but it's probably also going to be more didactic.

Yeah, I'd start with the quick version sans library, and only create the more challenging library if it seems particularly useful. Another pattern I would use would be too factor out common properties:

struct Creature {
   class: Class,
   race: Race,
   hitpoints: int,
   sprite: Whatever,
   armor: int,
   ...
}

enum Class {
   Warrior, Theif, ...
}

I suppose you were likely already going to drop that, but if you take enough information out of the enum you can make more of your case entirely general without any traits or other abstraction required.

1 Like

Do you have any reanson to reccommend ggez over piston? I never used any of them, so I myself have no preference…

Compared to Piston, ggez is much simpler and more idiomatic to Rust, and those that have experience with the LÖVE game framework will feel right at home with it. If developing a 3D game, on the other hand, I'd likely opt for Piston, or something else. Ggez is geared specifically for 2D games.

1 Like

Never used the LÖVE game framework (nor any other, for what matters) but simplicity is a big plus. I am targeting a as-simple-as-possile 2D graphics, so ggez is more than adequate in that sense. I'm just a bit worried because the project is small, I hope it doesn't get discontinued in the future.

Anyway, ggez seems good. I'll be trying to keep the core mechanics as independent as possible, so that a future change of engine would not be a drama. I already started experimenting and I got nice results (read: created a window and draw something basic on it).

Screenshot, just because…

The 'R' is (clearly) a Ranger, cells of different colors are different kind of terrain. And yes, the Ranger moves around. That's pretty much all at the moment.

2 Likes

Small update: now I can insert into play multiple units belonging to different team. They can move (with number of steps depending on the type of terrain and the class of the character) and attack each other (the red dot in the image is the cursor to select the target of the attack). Each kind of unit has a different attack, characterized by how far the range reaches and how much damage it inflicts.

Unfortunately, this is only a hacked proof-of-concept, poorly designed and full of bugs. I will have to rewrite everything once I figure out how to best implement the game.

Sounds like a good first iteration to me :).

1 Like