Hacker Newsnew | past | comments | ask | show | jobs | submit | thebracket's commentslogin

I'm not sure what promos are active right now (I just got back from vacation). It will be included in the November Thanksgiving sales. You could also email support@pragprog.com - they would be able to help.


I'll reach out to them, thanks!


Page count was a real concern. I wanted to introduce Rust newcomers to easy concurrency, and keep the data-storage side of things manageable (storing a big list of dynamic objects with traits gets messy fast and leads to a lot more borrow-checker fighting). Using an ECS let me dodge the latter bullet, at the expense of a bit of complexity. (I made sure Flappy didn't need an ECS)

Bevy didn't exist when I started writing, or I'd have probably used it. Legion is a great ECS, but it's heavier than I'd like - and Bevy makes a lot of things really easy.


This concerns me a bit.. when I've made (failed) attempts to learn Rust in the past the borrow checker was always the sticking point.


It needs a mindset change. The thing is trying to help you. It points out issues that you didn’t think about. It’s naturally difficult to think about things you’ve never had to think about before. It also turns out that architecting software in certain ways (like ECS) makes it easier to think less about borrowing.


(Author here) Bob makes some good points, so I'd like to share my $0.02 on the ECS debate. The posters below who point out that a lot of Rust setups use ECS to avoid mutability issues are correct (although internally Bevy is an ECS that maps its own node graph) - and that certainly helps - but it's not the whole picture.

I think it's important to separate the EC from the S in ECS. Entity-Component storage is basically a fast, in-memory database. It's a great way to store global state, and provides for really efficient querying. Using it as a database gives you some advantages:

* Composition over inheritance (especially in Rust, which doesn't really have inheritance - although you can fake it with traits). It becomes easier to glue on new functionality without realizing that you need to rearrange your object tree, and there's real performance boosts to not doing virtual function calls or dynamic casting to see what an object is.

* Replication; if you want to replicate state across multiple nodes, a good ECS can really help you.

* Mutability; as mentioned above, you don't need mutable access to everything at all times, and your code is definitely safer if you have explicit mutability control.

* Surprising flexibility; Rust EC setups typically let you put anything into a component. I have one slightly crazy setup that stores an Option<Enum> as a component with the enum featuring a number of different union setups.

So what about systems? Sometimes systems have some real advantages:

* For simulation type games, it's great to be able to add a simulation feature and have it apply everywhere. For example, when I added gravity to Nox Futura it instantly worked for player characters, NPCs, and objects. (It also worked on flying creatures, killing them instantly - but I fixed that).

* It really helps with parallelism. Your systems declare the data to which they will write, allowing the ECS to order your systems in such a way that you get parallelism without having to think about it too much (especially in Rust).

* If you're in a team, it's a great way to break out work between team-members.

* It's often helpful for finding bugs, because functionality of one type is localized to that system. You can get the same result by being careful in a non-system setup.

Sometimes, systems aren't so great. It can be really tricky to ensure that linked events occur in the correct order. You can make a bit of a mess when you want something to work one way for one type of entity and another for a different type.

But here's the thing: the systems part is optional. You can easily have your main loop query the EC data-storage directly and work like a traditional game loop - without losing the benefits of the storage mechanism. If you prefer, you can attach methods to components and call those. Or you can build a message-passing system and go that way. There's no real right way to do it. Once you've got the hang of your ECS's query/update model, you can tailor the game logic however you want. (I happen to like systems, but that's a personal choice more than a "you must do this" belief).

(Edit: My formatting was awful, sorry.)


> Composition over inheritance (especially in Rust, which doesn't really have inheritance - although you can fake it with traits)

This does not require ECS, you can happily have something like

  struct Entity {
      components: Vec<Box<dyn Component>>,
  }
(Nor do I think this is a good way of setting up a traditional roguelike)

> Replication; if you want to replicate state across multiple nodes, a good ECS can really help you

I'm not sure what you exactly mean by nodes here, but making something serializable in Rust for easy replication is hardly an issue when we have access to tools like serde.

> Mutability; as mentioned above, you don't need mutable access to everything at all times, and your code is definitely safer if you have explicit mutability control

Addressed above, but while ECS solves the mutability issue it's not a unique way of solving it and bringing it in to deal with that is overkill at the very least.

> Surprising flexibility; Rust EC setups typically let you put anything into a component.

Again, you can do this with regular old components too. This is also an oversold feature of components / mixins / anything like this in general, I think. You can never "just add a component", you need to fix all the issues that come with that, like making sure that gravity doesn't kill your birds.

> For simulation type games, it's great to be able to add a simulation feature and have it apply everywhere.

Yes, but that's not what a turn based tile based game is. Generally you want to iterate over things in order - gravity (if a roguelike has such a thing) gets applied on the player's turn, and only for the player. If you step over a ledge you don't wait for the "gravity" system to kick in and apply gravity to all entities, it is resolved in the same instant for only the entity that has just moved

> It really helps with parallelism.

Sure, although gameplay logic is not what's going to have to be parallel in a roguelike. Building up pathfinding maps and similar is useful to do in parallel, but ECS doesn't really help you with that.

> If you're in a team, it's a great way to break out work between team-members.

Not a particular strength of ECS, and if anything I could see issues arising from the fact that you basically have dynamic typing when it comes to what behaviors an entity has.

> But here's the thing: the systems part is optional.

If you're not doing query-based ECS with systems there's also no particular reason to not use vecs of components within entity structs.

I believe what you're doing here is adding a bunch of complexity to something that could be much simpler, and it really does the language a disservice.

As a final note, in the excerpt about items you have this justification for not using an enum instead of components:

  Each item you’re adding provides only one effect. It’s tempting to
  create a generic UseEffect component containing an enumeration.
  Enums can only have one value—if you want to make an item with
  multiple effects, you’d be out of luck. It’s a good idea to separate
  effects into their own components in case you decide to create an
  item that does more than one thing.
Not only does this violate YAGNI, it's trivial to work around:

  enum ItemEffect {
      Heal(i32),
      Poison(i32),
      Explode,
      MultiEffect<Vec<Box<dyn ItemEffect>>>,
  }
It just feels like you're looking for problems to solve.


    struct Entity {
        components: Vec<Box<dyn Component>>,
    }
At this point you're effectively just hand-rolling your own ECS - and perhaps being in denial about it by touting the fact that the ECS is half implemented at best - rather than eschewing an ECS outright, IMO. If you want to skip the ECS, embrace the natural typing of the language - then you don't need to build something to query and filter components, and can instead just use the language's built-in constructs:

    struct Entity {
        pub position: Option<XY>,
        pub sprite:   Option<SpriteId>,
        pub scripts:  Vec<ScriptRef>,
        //...
    }
    
    fn render_world(entities: &[Entity]) {
        for entity in entities {
            if let Entity { position: Some(xy), sprite: Some(sprite), .. } = entity {
                // ...
            }
        }
    }
Entity may eventually become a merge hazard on larger teams, and Entity enumeration without archetype filtering may eventually become a performance hazard if done too frequently and naively, but this kind of approach can work fine for many smaller projects.


The crucial difference between having a vector of components is in access patterns - in ECS you iterate over entities that have a given set of component, using queries, with entities that have vectors of components you're still iterating over entities. For something like a roguelike you're generally just dealing with one entity at a time, so this is a very important distinction.

That said, I don't think just having a vector of components is good design, you can do a lot better than that. I think the most natural thing to do in most roguelikes is to have an "Entity" type that's shared between players and monsters and has things that you'd expect those things to have, along with an inventory and potentially some kinds of tags governing behavior. Type Objects etc fit nicely into that kind of scheme.

>and Entity enumeration without archetype filtering may eventually become a performance hazard if done too frequently and naively, I'm specifically talking about turn based tile based games, here, so iterating over all entities matching a given set of components is rarely going to be a performance concern. It's far more important to make sure you're efficient when it comes to things like AI routines and pathfinding.


We should probably agree to disagree; your approach is just as valid as mine. My first attempt at a roguelike in Rust ( https://github.com/thebracket/rustyroguelike ) was a straight-from C++ approach, using dynamic traits and a lot of casting. It was my first try at using Rust, and is pretty bad - but at least it completed the "r/roguelikedev does the tutorial" event. Going from OOP to a non-inheritance language was quite the leap.

You absolutely can have an entity structure containing a vector of dynamic component types. It can get quite messy when you start having a lot of component types. When your entity runs, it has to look at the data in its components. That either means that component is a big enum (no need for dyn and Box there), or component is a trait and you're going to be doing a bunch of dynamic casting to find out what components an entity has when it ticks. Either can work (as can having an Option for each component type, which replaces the dynamic cast with an `if let`). Ultimately, there's not a lot of difference between an entity iterating its components and calling methods on components (or branching based on the components it has) and running a query to retrieve the components you want and acting on them. It's not that different from a NoSQL database (everything in a node graph) vs. a relational database (tables keyed to an identifier) - both work, each has their strengths and weaknesses. (I also really didn't want to try and teach dynamic casting early in the book!)

Some specific points from your reply:

By "replication", I meant network replication. You can get the same benefits by tracking changes within an entity/component, but it's really handy when a single storage system does that for you. Not that useful to a traditional single-player roguelike, but a nice tool to have.

> Yes, but that's not what a turn based tile based game is. Generally you want to iterate over things in order - gravity (if a roguelike has such a thing) gets applied on the player's turn, and only for the player. If you step over a ledge you don't wait for the "gravity" system to kick in and apply gravity to all entities, it is resolved in the same instant for only the entity that has just moved

That very much depends upon what you're creating. (Nox Futura is a Dwarf Fortress like). In the gravity example, it was solved by remembering to exclude the Flying component from the gravity query. The roguelike example in Hands-on Rust actually runs the player and monsters in different phases, but re-uses a lot of systems in the process. Matching on the turn state and executing systems isn't all that different to matching on the turn state and running tick functions on the entities included in that turn - especially if you have an energy cost/initiative type system breaking out the moves (the tutorial I created does this). Both Hands-on Rust and the tutorial effectively use message-passing for chaining events together.

> if anything I could see issues arising from the fact that you basically have dynamic typing when it comes to what behaviors an entity has

You have the exact same problem with a dynamic vector of components.

> If you're not doing query-based ECS with systems there's also no particular reason to not use vecs of components within entity structs.

It mostly boils down to preference. I find Query<(MonsterAI, Position, LikesToEatPlayers)> easier to work with than having each entity iterate a component list, query the type of each component and then act accordingly.

> As a final note, in the excerpt about items you have this justification for not using an enum instead of components:

Again, we're solving the same problem in similar ways. There's really not a whole lot of difference between matching the enum and iterating MultiEffect and querying if an entity has components. Either way, you have code that says "oh, it explodes" and makes a boom.

In other words, we both prefer different ways to accomplish exactly the same thing. Neither of us is right or wrong, there's plenty of ways to skin a cat. Hands-on Rust is as much about teaching Rust and gamedev in general as it is roguelikes in particular; if you want a big, working roguelike - the tutorial ( https://bfnightly.bracketproductions.com/ ) does that.


(Author here) That's the Roguelike tutorial I created, not the Hands-on Rust book. The two are quite different beasts, with a bit of overlap.

Hands-on Rust is designed for the newcomer to Rust, and carefully maps tutorial sections through teaching beginner-to-intermediate Rust concepts. It starts with some basic Rust exercises, works through a Flappy Bird clone, and then uses Roguelike development to teach a lot of underlying Rust concepts. It also teaches gamedev, and tries to do so in a way you can reuse in other games.

The tutorial is all Roguelike, all the time - focused on building a working roguelike.


Congratulations on releasing your book. I watched one of your talks about procedural generation[0] and really enjoyed it, thanks!

Mentioning this here since the tutorial linked above has a lot of procedural generation content

[0] https://www.youtube.com/watch?v=TlLIOgWYVpI


Thanks for the explanation. I'll get that clarified/cleaned up in the tutorial soon. Tracking it on the associated Github as issue #73, so I don't forget.


It's the standard "mdbook" that ships with Rust, also used for the Rust documentation. I pretty much suck at HTML, so I went with the easy markdown option and am enjoying the benefits!


Thanks for that! (I'm Herbert, made the tutorial; took a weekend away from the computer and it hit HN - nice surprise!). I definitely need to go back and fix a lot of my early Rust (it gets better) and some explanations like that. I welcome bug reports and PRs on the GitHub ( https://github.com/thebracket/rustrogueliketutorial ) if you find more.


Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: