PistonDevelopers / piston

A modular game engine written in Rust
https://www.piston.rs
MIT License
4.61k stars 236 forks source link

Keep `Game` trait or use iterators only? #212

Closed bvssvni closed 10 years ago

bvssvni commented 10 years ago

One problem is after redesigning the iterator to take advantage of the stack environment, they will no longer be isomorphic. You can't take a game written with the Game trait and translate it to using iterators by just copying and pasting code.

bvssvni commented 10 years ago

This issue was opened to address the concerns by @dobkeratops https://github.com/PistonDevelopers/piston/issues/207#issuecomment-44553860

bvssvni commented 10 years ago

When I compare https://github.com/PistonDevelopers/piston/blob/master/examples/image_iter.rs against https://github.com/PistonDevelopers/piston/blob/master/examples/image.rs I find the iterator much easier to read.

I have not come up with an example where using a Game trait brings more benefit. On the other hand, using an iterator has large benefits in terms of flexibility.

bvssvni commented 10 years ago

I believe using iterator it will make it possible to reduce the scope of the project while not sacrificing functionality, because assets can be handled in external libraries without needing to update Piston. This will work because the external libraries do not have to design something to work with Piston, but instead just designed for the stack environment which most libraries are.

dobkeratops commented 10 years ago

Perhaps the best approach will look different between a small sample, and a large, complete application; Does look like the 'iterator' case is easier to follow in this example because everything is in one place, but when the implementations grow you'd remove more from the main loop. Think about the nesting levels aswell (ok perhaps that won't be so bad if the iteration suits for loops)

maybe piston can support both approaches- one layered on the other ?

Is there an analogy to how MFC is built on the WIN32 api: the lower level win32 api is designed for C and switch statements dealing with messages - then MFC builds C++ classes with vtables around that;

The difference in Rust is - the powerful macro system might be able to roll all that boilerplate more conveniently - from what I've seen I think it should be possible to write a macro to generate functions with associated enum variants

bvssvni commented 10 years ago

I'll translate Rust-Snake to use iterators to do a comparison. However, there are drawbacks with the Game trait that can't be solved right now, for example how textures are handled.

The Game trait tries to solve the exact same problem as the iterator, but is strictly less flexible and does not scale the same way. With iterator we might have the opportunity to build something on top of it using inspiration from MFC.

dobkeratops commented 10 years ago

another consideration is multithreading - does the trait lend itself more to parallel update/render - or asynchronous asset loading (r.e. AssetStore)

bvssvni commented 10 years ago

It is easier to do parallel or concurrent computations in the stack environment because the user is not limited to handling the logic in one method.

The iterator can do everything the Game trait can do, it is a strict super-set in functionality. I have not come up with one counter example. It is simply the same thing but with stack environment.

dobkeratops commented 10 years ago

It is easier to do parallel or concurrent computations in the stack environment because the user is not limited to handling the logic in one method.

well just my opinion, ...

.. you could say parallel computations are easier when functions have clearly restricted access to the appropriate subset of data - e.g. mutable gamestate to 'update' , but no rendering assets; or immutable gamestate for 'render' .. and so on. Asynchronous loading is another case of concurrency to consider (r.e. 'AssetStore') ; This can all be enforced through the method signatures, exploiting rusts' immutability for better control.

you could say that the whole stack environment is a little like having 'globals' - a hazard which makes concurrency harder.

The benefit i see of the methods is you can do the work of arbitrating all of this controlled access in a framework and then a user plugs into the appropriate hooks & data structures ; its a problem for a closed source library if its opaque - but this is open source so we all have the source for reference and can add the the framework for our use-cases

so personally I find the trait more interesting - but I can also see the value of the 'match' dispatching for scripting - so having both would be great

bvssvni commented 10 years ago

Yes, it is like having 'globals', but there is no semantically difference between this and putting it in the application structure. In addition there are other limits you don't have in the stack environment:

You have more control in the stack environment, because you can make variables immutable if you want to. You can do asynchronous loading and what you like because you are not restricted to handling the callbacks in one function. You can handle the callbacks invariant of the event.

bvssvni commented 10 years ago

To make an application state flexible to handle powerful logic, you are forced to evolve it into a state machine. These are hard to debug and error prone.

The stack environment is better because it allows limiting the context to only the data you need. For example, you can make a game loop in a function that takes only the data you need for that scene. You don't have to put everything in the main loop.

If we use the Game trait, we are forced to put everything in the application structure and every library feature in AssetStore. While this is conceptually simpler, it is worse for the same reason a singleton is simple to understand but not good for scaling applications.

bvssvni commented 10 years ago

I am concerned about the scope of this project and want to move responsibility for special concerns out in external libraries. The Game trait puts all the pressure on the application state and AssetStore. This is why games use scripting languages, because they can't describe events in a sequential matter.

When I designed animation software that reused the same structure through the entire animation I ran into a similar problem. The user needs to put extra effort into switching context, there was no way to reset or reuse part of the existing one.

A stack environment is the ideal solution for controlling context. This is why scripting languages are popular, because you can name stuff and rename it and put it together when you need it. When you don't need it anymore it runs out of scope and there is no need for a garbage collector to release that memory.

dobkeratops commented 10 years ago

"The stack environment is better because it allows limiting the context to only the data you need."

i'd say the opposite - trait methods define clearly access to limited subsets of the data - like the 'AssetStore' only mutable for the async loading and whatever other interfaces may be needed (request queues?..) - writing concurrent applications is harder, and I think any difference between the stack environment & the game trait just reflects that;

Given the full stack environment, the user has to do the work of restricting :) Once the framework has done that work, its available to all users.

So you start have a stack environment... in which you build some concurrency, handing subsets to functions.... those functions become the new framework. The beauty of Rust is that traits can be instantiated as vtable or as static calls - a framework that takes a plugin trait can be adaptable and efficient

dobkeratops commented 10 years ago

"The Game trait puts all the pressure on the application state and AssetStore. This is why games use scripting languages, because they can't describe events in a sequential matter."

... so divide these up further. GameState, GameRenderState, AssetRequests, ImmAssetStore etc.. subsets that 'main' passes to user hooks... as many as are needed to solve the problem. Of course there is a root stack somewhere in the application.

Concurrency with asset loading will necessarily involve some sort of asynchronous allocations (you may of course separate that even further.. if need be separating allocation requests for some memory manager, which could in turn be part of asset streaming logic..)

event-scripting language and concurrency work on at different levels - one is content/UI, the other is systems/framework; they're definitely not mutually exclusive concepts... one can layer on the other

bvssvni commented 10 years ago

The issue here is whether one wants to build up or carve out.

The problem with traits is they are set in stone. They break the code when you change them. I don't want to design a library that breaks existing code just because we add a new feature.

To avoid this we need a more flexible structure. This introduces complexity and semantics that the user has to learn. This again leads to the requirement for more documentation.

All users know how to use the stack environment, so they don't need any documentation for that. It falls natural with the way they are used to program.

dobkeratops commented 10 years ago

I don't want to design a library that breaks existing code just because we add a new feature.

... equally you want to solve problems that make the library worth using: concurrency and cross platform are great opportunities for a framework to stand out

well, this would all be layered in stages. I don't think they should be mutually exclusive. you could have one layer of the library available in the stack environment, and build a concurrent/asynchronous asset-streaming main loop as a 'sample' .. which exposes new traits .. and becomes a library layer available to the user (like MFC built on Win32 )

bvssvni commented 10 years ago

In terms of value, you can consider the iterator design having lots of proven benefits. On the other side there is a marginal and unclear benefit for using a trait.

My problem is that I can't come up with a counter example. I would like to have one clear benefit of using a trait, something that can be demonstrated.

bvssvni commented 10 years ago

We are already building libraries like https://github.com/PistonDevelopers/rust-event which are promising and it turns out we don't need to stuff it all into Piston.

dobkeratops commented 10 years ago

Ok well perhaps what i'm thinking of could be another layer. I'm not sure I see iterators as particularly helpful for concurrency - they seem to suggest a serial interface.

So your concurrency would be happening at another level.

bvssvni commented 10 years ago

When I say "value" I mean taking a watch and note the time I expect to spend working on Piston instead of creating games.

I believe the iterator design will save massive amount of time and it is strictly superior to the trait. Everything you want to do with the trait can be done just as easily with the iterator design.

gmorenz commented 10 years ago

I would like to have one clear benefit of using a trait, something that can be demonstrated.

Default implementations, we have a run method, but if a game wants to it can write it's own instead.

I'm sure this isn't by itself sufficient reason to use a trait, but it is a benefit.

bvssvni commented 10 years ago

What you are saying is that the design encourage a particular programming pattern, which is true, but it does not prevent you from making pure physical based games.

bvssvni commented 10 years ago

Default implementations, we have a run method, but if a game wants to it can write it's own instead.

The same applies to the iterator design.

I would like an example where the trait has a benefit which the iterator design has none.

bvssvni commented 10 years ago

I agree that writing a better game iterator will be harder than writing a new run method using a trait.

gmorenz commented 10 years ago

How do you override the run method in an iterator design, it's split up into the different states in the non-overridable iterator isn't it?

bvssvni commented 10 years ago

Like the run method is a whole, you need to write the iterator from scratch.

dobkeratops commented 10 years ago

I would like an example where the trait has a benefit which the iterator design has none.

Concurrency :) an iterator is serial. Concurrency would be implemented on top of the iterator design. Would you expose that as another iterator? or a trait to plug in parallelizeable Update(), Render(), AssetStreaming() Network() methods ? I think the latter.

The benefit of traits is the same as building a templated framework in C++: you can still make a framework versatile, inserting statically linked code with no vtable, or conditional dispatch

bvssvni commented 10 years ago

Concurrency can be done to the stack environment that is aliasable. This can't be done with the trait because the whole state is either mutable or immutable.

bvssvni commented 10 years ago

You can extend a trait with new methods, but you can also do the same with the iterator by mapping the same Args types to another enum.

dobkeratops commented 10 years ago

This can't be done with the trait because the whole state is either mutable or immutable.

.. but the point is the trait methods ask for specific subsets of data - they needn't be methods of Game, passing the whole state. It should be split up. The first split is clear .. asset store vs the rest. But there would be even more - for example, particles that are purely graphical , not able to write to the gameplay state - would go into some separate 'RenderingState' , which is never available anything which can mutate 'GameState'. and so on..

I think once you start dealing with concurrency you will want to divide up whats going on into multiple subsets, and define several methods that each accept the appropriate parts

bvssvni commented 10 years ago

The stack environment is the ideal way of splitting data into subset, by enclosing the environment into a function.

You don't have this flexibility in the trait because you have to split up when designing the library, not when programming the game.

gmorenz commented 10 years ago

I think once you start dealing with concurrency you will want to divide up whats going on into multiple subsets.

Can this be done in a reasonable fashion with a trait, without making breaking changes every time a new subset is introduced?

bvssvni commented 10 years ago

No, that's a problem in computer science called the "expression problem".

https://github.com/PistonDevelopers/rust-graphics/issues/254

dobkeratops commented 10 years ago

but thats the point of a framework - some of this boilerplate is done for you.

Can this be done in a reasonable fashion with a trait, without making breaking changes every time a new subset is introduced?

Sure its going to evolve, but converge over time; I don't think the level of breaking changes would be difficult as , say, Rust itself :)

you can make everything late binding with messages (more like ObjC than C++), but this is at the expense of efficiency.

dobkeratops commented 10 years ago

ok -well if you're set on the Iterator design.. we can always build a trait or function based interface on top of it, solving different problems.

[ elsewhere i've solve the problem of running the same code on windows,iOS,Android, MacOSX/Linux which all have slightly different main loops.. a Java stub on Android, objC on MacOSX, and C/freeglut for win/mac/linux with straightforward extern "C" functions :) not even a trait ]

bvssvni commented 10 years ago

but thats the point of a framework - some of this boilerplate is done for you.

That does not make sense without context. Only because there is a thumb rule for something doesn't keep you from examining whether the assumptions of the thumb rule holds.

As I said earlier, the analysis seems to indicate that the iterator design is strictly better. I was surprised myself because I had bad feelings about it. But logically there should be nothing wrong with it and the rational thing is to choose it since it is a strictly better option.

bvssvni commented 10 years ago

If it was better in one way and worse in another way, then I would be more careful. But I can't find one axis which the trait design does something better. I think it is because it simply lacks stack environment, otherwise they are no different. This makes it very easy to compare them side by side and tell which is better.

bvssvni commented 10 years ago

I don't know how the iterator design will affect programming style. I can't say if games in general will be more crappy. What I am saying is that from a flexibility view, the iterator design is better, but it comes at a small constant performance cost. I want to pay that price to get the other huge benefits as:

etc.

dobkeratops commented 10 years ago

The REPL is a nice benefit - I'm all for that - which is why I suggest looking into a macro to roll both an enum/match dispatch and trait version of the same interface, one layer able on the other. From what I've seen I think you could declare an interface that looks like either, and a macro rolls the other (along with the 'match' dispatcher).

I'm pretty certain that when it comes to concurrency, the iterator & stack environment themselves will be a detail that you'll only want considered by another layer of code, which will be built ontop of the iterator. The interface to this would want to be functions, which themselves will be called asynchronously, not sequentially in an iterator loop {} . With rusts' generics, you have the benefit that this can still be versatile, plugging in specific user data structures and static dispatch. Besides that , there is no way to make concurrency sane other than dividing data up into multiple subset structures. Making everything nested as locals in one stack frame would just get out of hand.

so to summarise..

it seems like you're set on the iterator - fair enough.

I think there will be scope to build a concurrent framework ontop, which would have some sort of function/trait interface.

bvssvni commented 10 years ago

I am not arguing for only programming in the stack environment. I am all for building concurrency on top of the iterator design.

The problem with the trait design is that you can't build abstractions. It does not scale. When the function returns, the world is reset and the game logic can't make assumptions about the environment.

dobkeratops commented 10 years ago

"the trait design is that you can't build abstractions. It does not scale."

... I have faith that rust's traits will be superior to C++ class hierarchies when it comes to layering/extending behaviour; as far as I can tell, building abstractions is exactly what traits do.

Its true you may sometimes get breaking changes - but its not like that will happen every release - just when major oversights in the original design come to light. When you add methods, you can give them defaults - a user will only need to implement them if they really are relevant.

Perhaps you're just reacting badly to one attempt to build everything in a single Game trait? instead of perhaps having renderable, updateable, streaming components?

bvssvni commented 10 years ago

Its true you may sometimes get breaking changes - but its not like that will happen every release - just when major oversights in the original design come to light. When you add methods, you can give them defaults - a user will only need to implement them if they really are relevant

This seems to be arguing for using traits for the traits own sake. A type system is just a compile time enforced contract on a context. With Rust you can control how much to use the type system in the stack environment, there is nothing that gets in your way.

When you introduce a contract as the assumption of how all games should work, this is the point when you start running into problems. You are restricting the space of well written games. I would like to allow more bad games to be written if I can expand the space without sacrificing expressiveness, flexibility or performance.

bvssvni commented 10 years ago

I have now updated Rust-Snake https://github.com/bvssvni/rust-snake/blob/master/src/main.rs to use iterator. I removed the Game trait and moved them under the impl App section.

bvssvni commented 10 years ago

I will try to iterate on the design until it starts conflicting with the Game trait. Then we probably have more experience with it and will be in a better position to choose the best direction.

Closing this discussion for now.