this post was submitted on 18 Mar 2024
75 points (95.2% liked)

Transprogrammer

781 readers
1 users here now

A space for trans people who code

Matrix Space:

Rules:

founded 1 year ago
MODERATORS
 

I lived in a perfect OOP bubble for my entire life. Everything was peaceful and it worked perfectly. When I wanted to move that player, I do player.move(10.0, 0.0); When I want to collect a coin, I go GameMan -> collect_coin(); And when I really need a global method, so be it. I love my C++, I love my python and yes, I also love my GDScript (Godot Game Engine). They all work with classes and objects and it all works perfectly for me.

But oh no! I wanted to learn Rust recently and I really liked how values are non-mutable by defualt and such, but it doesn't have classes!? What's going on? How do you even move a player? Do you just HAVE to have a global method for everything? like move_player(); rotate_player(); player_collect_coin(); But no! Even worse! How do you even know which player is meant? Do you just HAVE to pass the player (which is a struct probably) like this? move(player); rotate(player); collect_coin(player, coin); I do not want to live in a world where everything has to be global! I want my data to be organized and to be able to call my methods WHERE I need them, not where they just lie there, waiting to be used in the global scope.

So please, dear C, Rust and... other non OOP language users! Tell me, what makes you stay with these languages? And what is that coding style even called? Is that the "pure functional style" I heard about some time?

Also what text editor do you use (non judgemental)? Vim user here

you are viewing a single comment's thread
view the rest of the comments
[–] bwrsandman 43 points 3 months ago* (last edited 3 months ago) (5 children)

If you want your code to be performant you need to think about how you lay out your data for your CPU to manipulate it. This case might work well for one player but what if you have 100, 10 000?

When you call player->move (assuming polymorphism), you're doing three indirections: get the player data at the address of player, get the virtual function table of that player, get the address of the move function.

Each indirection is going to be a cache miss. A cache miss means your cpu is going to be waiting for the memory controller to provide the data. While the cpu can hide some of this latency with pipelining and speculative execution, there are two problems: the memory layout limits how much it can do and the memory fetch is still orders of magnitude slower than cpu instructions.

If you think that's bad, it gets worse. You now have the address of the function and can now move your player. Your cpu does a few floating point operations on 3d or 4d vectors using SIMD instructions. Great! But did you know that those SIMD registers can be 512 bits wide? For a 4d vector, that's 25% occupancy, meaning you could be running 4x as fast.

In games, especially for movement, you should be ditching object oriented design (arrays of structs) and use data oriented design (struct of arrays).

Don't do

struct Player { float x, float y, float rotation, vec3 color, Sprite* head};
Player players[NUM];

Instead do

struct Players {
    Vec2 positions[NUM];
    float rotations[NUM];
    vec4 colors[NUM];
    Sprites heads[NUM];
};

You will have to write your code differently and rethink your abstractions but your CPU will thank you for it: Less indirections, operations will happen on data on the same cache lines, operations will be vectorizable by your compiler and even instruction cache will be optimized.

Edit 1: formatting

Edit 2: just saw you're doing 2d instead of 3d. This means your occupancy is 12.5%. That operation could be 8 times as fast! Even faster without indirection and by optimizing cache data locality.

[–] [email protected] 1 points 3 months ago (1 children)

Is this why Bevy's ECS works the way it does?

[–] bwrsandman 2 points 3 months ago

Yes ECS is probably the most popular scalable DOD programming pattern, aside from Compute Shaders. If correctly used, ECS store your data in a way that makes access more cache friendly. There are multiple flavours of ECS with some which are better for small components and access and others which are tuned for insert and delete.

One thing I would say if you want to switch to ECS is to start with a simple performance test of say 100, 10 000 and 1 million entities being updated in a loop. Do this with and without ECS. This way you can keep track of performance and have actual numbers instead of trusting the magic of ECS. ECS can have some overhead and aren't always the best choice and if you use them wrong they won't be as good.

I haven't tried Bevy yet but it looks very promising!

load more comments (3 replies)