Hacker Newsnew | past | comments | ask | show | jobs | submitlogin

https://easel.games

I'm making Easel, a 2D game programming language designed to match how humans, not computers, think about game logic. It also has automatic multiplayer. I've been working on it for 3 years!

Easel feels like a declarative programming language even though it is imperative, because lots of useful game-oriented features are first class. Like behaviours - you just say `on Pointer { ... }` and you have a concurrently-executing coroutine that's lifetime is managed. But you don't think about any of that complexity, you just think of your entity as having a behaviour and go forth and make your game.

It also happens to have automatic multiplayer. Normally with multiplayer you have to worry about doing everything in a "multiplayer safe" way (i.e. be deterministic and only modify the things your side has authority over). My idea was to put all the multiplayer stuff in the programming language itself, underneath all your lines of code. This way, anything you write in that programming language can just be made multiplayer, automatically. So you can just pretend all your players are in one shared world, like a singleplayer game, and the engine does all the multiplayer for you. It was really difficult to make but it makes multiplayer so easy for you now.

Easel is my idea of how games should be made, or at least as close to the idea as I can achieve with 3 years of work, and I would love for more people to try it out.



> you can just pretend all your players are in one shared world, like a singleplayer game, and the engine does all the multiplayer for you

But how does this really work? The website also says it's just baked into the language but there are many different approaches to networking games that have their own pros and cons.


It uses rollback netcode. The inputs are relayed to the other players and executed on all clients, and they end up in the same state because all Easel programs are guaranteed deterministic. To hide latency, the clients simulate forward even before they have received all inputs, and once inputs have been received it rolls back to the point of divergence to correct the prediction error. This works because the prediction is correct most of the time.

To be able to roll back, Easel incrementally snapshots the game state every tick. It only snapshots (and restores) what has changed, which makes it a lot more efficient than most rollback netcode implementations.

It also uses a peer-to-peer relay and adapts the latency asymmetrically, so the player who introduces latency feels their own latency.

I know there are other models and pros and cons, this is the right choice for Easel because I wanted to make the multiplayer fully automatic. One shared world, coded like a singleplayer game. There are certainly games which suit a client/server model better but I think the developer would then need to understand where their code is running and when to do remote procedure calls, and my goal was to make multiplayer so easy that even a teenager on their first day of coding could do it.


That's great stuff! IIRC Factorio takes the same approach but relies on extensive testing to avoid running into desync issues with non-deterministic code. Would be very cool to be able to build games like that without needing to worry about desyncs!

It might be a good idea to highlight some of the limitations to this approach somewhere so users aren't caught off guard later in the development process. For example, it wouldn't be great to build a competitive FPS or MOBA with this because the game state is replicated to all players which is a cheaters dream. The latency characteristics would also not be ideal for games with a larger number of players. I also assume there are no escape hatches for doing any non-deterministic things like I/O so there would be limited to no persistence possible. It won't be an issue for most games but worth highlighting just in case IMO.


Yes, I should have a "who is this not for" section somewhere, that's fair enough.

Persistence is actually handled by the server, and then the server inserts an input back into the game state with the result when it's done. So, no issue with I/O.

I see Factorio is doing deterministic lockstep, which like rollback netcode but without the rollback. That makes sense seeing as it is a game with a lot of state and so it would be too expensive to snapshot and rollback all the time. But yes, I think having a game engine which guarantees determinism in all cases could be a useful base for other multiplayer architectures too. Right now Easel only does rollback but maybe it could do more in the future.


This is really cool. Nice project!

I tried doing something much more rudimentary before. Will be following


Oh, thank you!

I would love to hear more about what you were trying to do with your project before. Was it more similar to the declarative coding part, the automatic multiplayer part, or something else? Part of why I'm doing this is to explore the design space of how games should be made and I'm interested to hear what problems, issues, pet peeves, "bugbears" etc that other people think are worth solving.


It's been a while. But I believe what caused me the most headache while trying to build something like this was handling the interactions between different elements. Declaring which objects were affected by "attacks" or could be "player interactive" or "affected by player but not by NPC". Really this boiled down to proper inheritance. But I found myself so deep and tangled a fresh reset would have been better. Then determining if the object itself or an "objective manager" should perform the calculation each cycle.. etc

It was messy. I ended up having NPC, Item, Attack classes and for each a NPC Manager, Item Manager, and Attack Manager to calculate all their interactions and states.

That's why your project seems interesting because it seems to handle the heavy lifting of behaviors and "behind the scenes".


Oh yes, handling interactions and dependencies and what is affected by what. I did a lot of React development (as in the frontend web framework) before making Easel and was quite inspired by how it hooks to change. The way you give it a little routine, it says what it depends on, and then it just fully re-executes that whole routine when the dependencies change. So in Easel when you say `with Health { ... }` it makes a behaviour that re-executes every time the Health changes. But, if it just reran the behaviour, then you'd end up with it adding a new sprite (for example) every time it re-executes, until you've got hundreds of them. So the other trick is the Easel compiler assigns an implicit ID to things like sprites so that it will replace rather than add the second time around. It's built into the programming language so you don't see it (most of the time). It actually took me 2 years to come up with that, which is both cool and depressing when I can explain it in one paragraph.


> So the other trick is the Easel compiler assigns an implicit ID to things like sprites so that it will replace rather than add the second time around

Is the ID computed based on the shape of the expression at runtime or on something else?


I found https://easel.games/docs/learn/language/functions/ids#implic... which suggests they are structure-based, at least, though loops aren't mentioned.

Great documentation, by the by!


The implicit ID is just an auto-incremented number actually, it's not anything too special. That means, if you have a loop, the component has the same ID each time and gets replaced. That is a feature, not a bug. So this code snippet will keep replacing the text sprite with a new one, counting from 1 to 10:

for i in RangeInclusive(1, 10) { TextSprite(i) }

Yes, you found the right place in the documentation. Thanks, yes I worked very hard on the documentation!


Ah, so you don't do parallel with implicit IDs right now, I'm guessing. Or you do, but it has some interesting bugs as the IDs shift around? Or the scope of the auto-incrementing number is lower than "the entire program" so parallel works?


It sounds like you are asking, if auto-incrementing IDs are assigned in parallel at runtime, then order of execution of the threads must affect which ID gets assigned to what, and that must make some interesting bugs?

The IDs are assigned at compile time by the Easel compiler. So they don’t change in any way at runtime. Does that answer your question?


Mostly - if the IDs are assigned at compile time, how do you deal with _runtime_ locations like "the fifth iteration of a For loop"?


Actually, all iterations of the for loop have the same ID. This is the design. So the second iteration has the same ID as the first iteration, which means it replaces the sprite created by the first. The fifth iteration has the same ID as the fourth iteration, so it replaces the sprite. So all the iterations keep replacing the same sprite. And that is how you animate sprites!

If you are actually trying to make multiple sprites and not keep replacing them, what you do is you spawn a new entity to hold your extra sprite:

for i in Range(0,10) { Subspawn { TextSprite(i) } }

That code creates 10 sprites and achieves what I think you are thinking of.


I've actually just recently been through this myself. I'm also building something for kids to build games in. But much more opinionated than Easel. It's interesting to me that Easel looks declarative but is imperative.

I've actually gone with a 100% declarative approach. Basically you define effects, which are executed in response to certain interactions. There's a comprehensive targeting system. But the best part is this is all type-safe using TypeScript, the declarative structure is enforced. That means even when you chain effects, nested effects are able to access (incl. autocomplete) the targets of parent effects etc. Whilst this provides a super nice experience to consume, it's definitely non-trivial to build this system.

https://breaka.club/blog/why-were-building-clubs-for-kids


Wow, impressive project! I like how you’ve focused on tooling and workflow, it makes sense that is where your most important problems are. Cool QR code drawing template idea too :)




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

Search: