thoughtbot / write-yourself-a-roguelike

Write Yourself A Roguelike: Ruby Edition
MIT License
155 stars 11 forks source link

Move to an Entity-component-system #37

Open ngscheurich opened 6 years ago

ngscheurich commented 6 years ago

As @Trevoke pointed out, using a more modern architectural pattern than that used in NetHack could likely make the book easier to follow as well as to write. (Not to mention, I don't believe anyone here has much familiarity with the NetHack source code.)

Entity-component-systems were introduced by game developers in the late 90s to solve the problem of reasoning about and managing complex, intertwined game systems. Since then ECS has become, I'd venture to say, the de facto implementation for games.

Thoughts on moving over to an ECS before we get much further on this project?

sardaukar commented 6 years ago

I wonder if starting with an ECS is a bit much for a newbie intro to roguelikes. Or is it actually simpler (in complexity) but just weird to wrap your head around?

Trevoke commented 6 years ago

It's a good and important question.

Simply speaking, an ECS is a way to manage complexity, like all other patterns. It means entities are just bags of components, components are bags of properties, and systems operate based on what components are on an entity.

There's a few factors to keep in mind:

  1. I'm building an ECS in Elixir right now so my mind is severely biased towards it
  2. We're probably not building a full-fledged Nethack clone so we may not need that flexibility
  3. If we don't do something like this, are we doing the readers a disservice? To which extent will the book be instructive, interesting, and preparatory if they want to build their own game, which WILL have this need for flexibility?
  4. If we DO something like this, are we just repeating information people can find somewhere else? Or are we providing value because all this information is gathered together under the "Create a roguelike" banner?

One of the good news about what we're doing is that it's turn-based so we don't have to build a full-fledged ECS.

I'm going to check out the Nethack codebase and see how much I can map what we have of this book to what is in there.

Here's a super simple, super dumb, not-at-all-thought-out(*) sketch for an ECS in Ruby:

class Entity
  attr_accessor :components
  def initialize(components = [])
    @components = components
  end
  def find(klass)
    @components.find {|x| x.class == klass }
  end
end

class Human < Entity
  def initialize(components)
    super(components)
  end
end

# These are probably better as Structs
class Stats
  attr_accessor :int
  attr_accessor :wis
  attr_accessor :str
end

class Resources
  attr_accessor :health
  attr_accessor :mana
end

class Poison
  attr_accessor :health_mod
end

module PoisonSystem
  def call(entities)
    entities.filter!(&:poisoned?)
    entities.each do |entity|
      resources = entity.find(Resources)
      poison = entity.find(Poison)
      resources.health -= poison.health_mod
    end
  end

  private

  def poisoned?(entity)
    entity.components.any? { |x| x.class == Poison }
  end
end

human = Human.new
human_stats = Stats.new
human_stats.int = 12
human_resources = Resources.new #har, har
human_resources.health = 5

human.components << human_stats
human.components << human_resources

poison = Poison.new
poison.health_mod = 2
human.components << poison

PoisonSystem.call([human])

*: where not-at-all-thought-out means "I've spent over a year thinking about what an ECS needed to be like so the pattern's kinda clear in my head now"

Trevoke commented 6 years ago

Here's the logic for "poisoned" in Nethack: https://github.com/NetHack/NetHack/blob/024e9e122576db664e37df0937cfb4c06c436e0c/src/attrib.c#L255

It's readable, but it's also fairly procedural.

Right above it is this function:

/* feedback for attribute loss due to poisoning */
void
poisontell(typ, exclaim)
int typ;         /* which attribute */
boolean exclaim; /* emphasis */
{
    void VDECL((*func), (const char *, ...)) = poiseff[typ].delivery_func;
    const char *msg_txt = poiseff[typ].effect_msg;

    /*
     * "You feel weaker" or "you feel very sick" aren't appropriate when
     * wearing or wielding something (gauntlets of power, Ogresmasher)
     * which forces the attribute to maintain its maximum value.
     * Phrasing for other attributes which might have fixed values
     * (dunce cap) is such that we don't need message fixups for them.
     */
    if (typ == A_STR && ACURR(A_STR) == STR19(25))
        msg_txt = "innately weaker";
    else if (typ == A_CON && ACURR(A_CON) == 25)
        msg_txt = "sick inside";

    (*func)("%s%c", msg_txt, exclaim ? '!' : '.');
}

If I read this correctly, it's impossible to get to the max value of at least strength or constitution without using those items, so checking for max value means you're using the items, and therefore the value can't change, and therefore you get a different message.

ngscheurich commented 6 years ago

As I thought about architectural patterns, and what is appropriate for the book, and what the example project will look like, I had a couple of thoughts:


Something that came to mind as a good way to determine the answer to "is an ECS a bit much for our readers" was:

Perhaps I could write up a quick excerpt, as it might appear in the book, explaining ECSs and we could use that as a barometer for how easy the concept is to elucidate to our readers.

I soon thereafter realized that there's a pretty important question that I feel has sort of been implicitly answered but deserves a but more discussion: Who are our readers? Who is the target audience for this book?

Do we expect that they have some familiarity with programming? With OOP concepts? With Ruby? I know that the existing bits of the book hint at the audience, but we should solidify a good answer to "Who is this book for?" (We should also include a brief explanation of this in the book itself!)


I assume we expect that a reader who follows along with the book will end up with a fully functional game. So, to ensure a functional end product, do we develop the game and then write the book? Write the book as we write the game? I've no experience with a book/project combo like this so I'd be interested to hear what y'all think.

sardaukar commented 6 years ago

I think a good approach is to assume Ruby knowledge, and some exposure to programming so not absolute beginners.

Writing the game beforehand seems to me the most sensible approach. That being said, we could tap into libtcod (https://bitbucket.org/libtcod/libtcod/wiki/Features) as it might make it easier for newcomers to roguelikes. Then again, maybe people interested in the book want to do those things themselves.

Trevoke commented 6 years ago

The major draws for this book for me were:

I think as much as possible we should build a simple version of things ourselves because coming to this book, I think people will expect to not be handed any magic libraries.

On Fri, Jan 19, 2018, 08:48 Bruno Antunes notifications@github.com wrote:

I think a good approach is to assume Ruby knowledge, and some exposure to programming so not absolute beginners.

Writing the game beforehand seems to me the most sensible approach. That being said, we could tap into libtcod ( https://bitbucket.org/libtcod/libtcod/wiki/Features) as it might make it easier for newcomers to roguelikes. Then again, maybe people interested in the book want to do those things themselves.

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/thoughtbot/write-yourself-a-roguelike/issues/37#issuecomment-358970656, or mute the thread https://github.com/notifications/unsubscribe-auth/AAEJSWaC5Avn0ICc4eAbpf7yegYKr1qlks5tMJ0zgaJpZM4Rja4R .

ngscheurich commented 6 years ago

I tend to agree with @Trevoke re: libtcod; although it's a great library, it will abstract away many of the mechanics that I was hoping this book would cover.

ngscheurich commented 6 years ago

Upon further reflection, I also agree with @sardaukar's sentiment that writing the game, or at least the basic systems that will be in place, before we start writing the book makes the most sense.

tvanderpol commented 6 years ago

Writing the game, or at least a skeleton of it, before writing the whole book might make sense, but I'd love to capture any dead ends and wrong assumptions as they happen.

Some of the most valuable insights in other programming books and video series that have focussed on building a project to completion have been the ones where you build something out a little bit, realise a shortcoming and refactor it into a more coherent whole.

Tying this back in to the ECS discussion - the chapters currently written about how to manage the player character stats and such seem good. I'd be very interested in a later chapter that shows the shortcomings of this method and refactor what's already there into an ECS based approach (maybe once status effects are introduced? I'm just making a guess here as to what ECS is meant to help with). I'd be much less interested in going back to these initial chapters and rework them to already have hit this solution. The first way will show as well as tell about the benefits of adapting the code to fit this framework while the second way does something similar to dropping in libtcod* out of nowhere - telling the reader something is valuable without truly doing the legwork to show why.

* in my own noodling in this space I've found libtcod to be a great library and we could certainly take a lot of inspiration from it in terms of what this book is here to teach!

sardaukar commented 6 years ago

How shall we proceed with the planning for the game, then?

Trevoke commented 6 years ago

The primary value that ECS derives is to split data from logic. And we can choose to limit how we use it, but one example of code flow would be, given that this is a turn-based game:

  1. player presses the "right arrow" key
  2. game catches input, disables player input temporarily, and parses "right arrow"
  3. game realizes there's a mob to the right of the player, so "right arrow" becomes "attack right"
  4. game runs through all relevant mobs / entities (all on this floor for instance) and figures out what their actions are
  5. game runs through all relevant mobs and figures out what changes need to be made based on components (e.g. poison)
  6. 3 + 4 + 5 = every system that needs to be run
  7. game runs "tick", that is, run every system that needs to be run on every entity that needs it
  8. apply final changes, resolve things like deaths
  9. display changes on the screen
  10. enable player input
tvanderpol commented 6 years ago

I think the answer to @sardaukar 's question on how to proceed planning the game is roughly this:

Step 1: Decide if we want to keep the code that's currently there (I would strongly vote in favour of this!) Step 2: Finish a dungeon generator - we can use something like the famous python roguelike tutorial as a base, or take Nethack's methods or whichever Step 3: LoS, functional navigation of the play space by the player character

After this step, we just basically go down the list of planned chapters as on the current home page:

I've left off the future projects and potential chapters stuff, if that's something we'd want to get into I personally feel it might be better to do those through refactors and additions to the above, not by adding them in from the start. To re-iterate my previous point, a large chunk of learning from coding books and videos for me has always been to see how people adapt existing code to do more stuff, not see it spring pristine from a copy/paste buffer somewhere.

I think it might be worth making sure that as the game is built, there's extra focus on keeping the commits both small and very well documented. This would be in service of structuring the write-up about these parts of development along the lines of how development actually worked and also to conserve useful mis-steps for detailing in the book (after all, if this group makes a wrong assumption about how to implement something, it stands to reason a reader might as well).

Trevoke commented 6 years ago

I agree with this overall direction, and I also think we should keep the current code.

ngscheurich commented 6 years ago

I'm all for keeping the current code; I really like @Trevoke's assertion about watching the code evolve over time.