0

I'm trying to write a simple game engine, but the normal patterns I would use to do this don't work here due to the strict borrowing rules.

There is a World struct which owns a collection of Object trait objects. We can think of these as moving physical objects in the game engine. World is responsible for calling update and draw on each of these during each game tick.

The World struct also owns a collection of Event trait objects. Each of these events encapsulates an arbitrary piece of code that modifies the objects during the game ticks.

Ideally, the Event trait object has a single method do_event that takes no arguments that can be called by World during each game tick. The problem is that a particular Event needs to have mutable references to the objects that it modifies but these objects are owned by World. We could have World pass mutable references to the necessary objects when it calls do_event for each event, but how does it know which objects to pass to each event object?

If this were C++ or Python, when constructing a particular Event, I would pass the target of the event to the constructor where it would be saved and then used during do_event. Obviously, this doesn't work in Rust because the targets are all owned by World so we cannot store mutable references elsewhere.

I could add some ID system and a lookup table so World knows which objects to pass to which Events but this is messy and costly. I suspect my entire way of approaching this class of problem needs to change, however, I am stumped.

A sketch of my code:

trait Object {
    fn update(&mut self);
    fn draw(&self);
}

trait Event {
    fn do_event(&mut self);
}

struct World {
    objects: Vec<Box<dyn Object + 'a>>,
    events: Vec<Box<dyn Event + 'a>>,
}

impl World {
    fn update(&mut self) {
        for obj in self.objects.iter_mut() {
            obj.update();
        }
        for evt in self.events.iter_mut() {
            evt.do_event();
        }
    }
    fn draw(&mut self) { /*...*/ }
}

struct SomeEvent<'a, T: Object> {
    target: &'a mut T,
}

impl<'a, T: Object> Event for SomeEvent<'a, T> {
    fn update(&self) {
        self.target.do_shrink(); // need mutable reference here
    }
}

I know that RefCell enables multiple objects to obtain mutable references. Perhaps that is the direction I should go. However, based on what I learned from the Rust book, I got the sense that RefCells break some of Rust's main ideas by introducing unsafe code and circular references. I guess I was just wondering if there was some other obvious design pattern that adhered more to the idiomatic way of doing things.

Shepmaster
  • 388,571
  • 95
  • 1,107
  • 1,366
sicklybeans
  • 299
  • 3
  • 11
  • *I could add some ID system and a lookup table* — congratulations, you've discovered [entity component systems](https://en.wikipedia.org/wiki/Entity_component_system#Game_example), not a hack. – Shepmaster Jan 06 '20 at 18:44
  • It looks like your question might be answered by the answers of [Need holistic explanation about Rust's cell and reference counted types](https://stackoverflow.com/q/45674479/155423). If not, please **[edit]** your question to explain the differences. Otherwise, we can mark this question as already answered. – Shepmaster Jan 06 '20 at 18:46
  • See also [Why can't I store a value and a reference to that value in the same struct?](https://stackoverflow.com/q/32300132/155423); [Mutable self while reading from owner object](https://stackoverflow.com/q/28385339/155423); [Passing mutable self reference to method of owned object](https://stackoverflow.com/q/30681468/155423); [Mutable reference to container object within iterator loop](https://stackoverflow.com/q/40196586/155423). – Shepmaster Jan 06 '20 at 18:48
  • 1
    And now for something completely different: A useful answer. https://github.com/amethyst/specs – user2722968 Jan 06 '20 at 18:50
  • *`RefCell`s break some of Rust's main ideas by introducing unsafe code* — `RefCell` introduces the same amount of unsafe code as `Vec` does — are you ok with using `Vec` or `String`? *and circular references* — `RefCell` does not allow for circular references; perhaps you are thinking of `Rc`? – Shepmaster Jan 06 '20 at 19:04
  • Because this question in its current form appears off-topic (asking for "the right design pattern" is opinion-based, at best), you may wish to look at other resources. More open-ended questions and discussions are welcome on [the Rust users forum](https://users.rust-lang.org/), [the Rust subreddit](https://www.reddit.com/r/rust/), [the Rust Discord server](https://www.rust-lang.org/community), or [the Stack Overflow Rust chat](https://chat.stackoverflow.com/rooms/62927/rust). – Shepmaster Jan 06 '20 at 19:06
  • 1
    I'm torn. It does read as opinion-based but I do feel that architecture questions are on-topic, and this one identifies a specific problem with enough detail that one could make a suggestion. Perhaps it could be rephrased to focus on "How do I do this?" rather than "How do I do this *in the best way*?" (obviously, when asking a question you always want the best answer). – trent Jan 06 '20 at 20:16
  • 1
    Be sure to watch [kyren's RustConf 2018 talk on using Rust for game dev](https://www.youtube.com/watch?v=aKLntZcp27M); it deals with a lot of the architectural decisions you are likely facing. – trent Jan 06 '20 at 20:22
  • Yes, this is on a pretty fine line, and some editing might clearly push it one way or the other. – Shepmaster Jan 06 '20 at 20:51
  • The original version of the question didn't mention `RefCell`, but when I [pointed it out to the OP](https://stackoverflow.com/questions/59617276/having-trouble-with-proper-design-pattern#comment105398052_59617276) they rejected it as a possible answer. It would be good for the OP to preemptively [edit] to put **all** the arbitrary restrictions they have so that any answerer doesn't waste anyone's time. – Shepmaster Jan 06 '20 at 20:53

1 Answers1

1

After reading the question comments and watching kyren's RustConf 2018 talk (thanks trentcl), I've realized the OO approach to my game engine is fundamentally incompatible with Rust (or at least quite difficult).

After working on this for a day or so, I would highly recommend watching the posted video and then using the specs library (an implementation of the ECS system mentioned by user2722968).

Hope this helps someone else in the future.

Shepmaster
  • 388,571
  • 95
  • 1,107
  • 1,366
sicklybeans
  • 299
  • 3
  • 11