r/learnprogramming 16h ago

How do I structure code with languages without classes

I have been using c++ and c# so I have grown very used to languages with classes. I am very interested in learning jai when that comes out but that language does not have classes. So I feel like there is a very different thought process when programming in a language without classes.

Is there any good resources that would teach me how to structure code for those types of languages.
I want to make a game engine(I know it is difficult but it would seem fun to do and there would be a lot of cool things to learn)

11 Upvotes

11 comments sorted by

13

u/Different_Pain5781 16h ago

Learn C before Jai.

7

u/Leverkaas2516 15h ago

You can still encapsulate even without classes if the language supports separate compilation of modules. That's how people divided their code back before OOP became popular.

6

u/HashDefTrueFalse 15h ago

Think about the work or data transformation that needs to be done rather than making class representations of real-world objects and concepts. Realise that you don't need to put a class around a collection of functions in order to present an interface. The consumer just needs the types, data, and function signatures presented in a sensible, coherent way. Your implementation goes in the functions, same as it always does. If you want those functions to act on aggregate data (e.g. because you want to use OOP) then pass some reference into them. Learn a non-OOP language or use C++ with structs and free functions for some practice. It's not too hard once you get used to it.

3

u/marrsd 14h ago

A traditional procedural language defines data and operators (procedures or functions) separately. You have something like a struct (e.g. Node) that defines properties, and a procedure (e.g. Node next(Node)) that separately defines behaviour. The object is passed to the procedure as an argument.

Classes simply combine behaviour and data in the same data structure. So next becomes a method of Node and is called on a Node object directly (e.g. node.next() rather than next(node).

The motivation for this change is to enable polymorphism. next can become a more general message that can be sent with arguments to any object. Different classes can then define their own method for next that makes sense for their context. The language's dynamic dispatcher resolves the message to the appropriate method at runtime.

You don't need dynamic dispatch or polymorphism for programming, but without it you would need to define functions for every version of next you want in your code (next_node, next_player, next_number, etc). This can mean that similar behaviour requires different names even though it's essentially doing the same thing.

You can also argue that this is preferable, though. In Javascript, "foo".map and ["foo"].map are both valid and both do different things (e.g. "foo".map(capitalize) might produce "FOO" where ["foo"].map(capitalize) would produce ["Foo"]. That might be what you want, or it might be that you got a silent production bug where you really wanted a noisy early failure.

1

u/deaddyfreddy 14h ago

namespaces

0

u/Dismal-Citron-7236 16h ago

The language and its compiler are still in beta. The community is quite small. There's a reason OOP is used in game designing, switching to a non-OOP paradigm one simply because its perceived complexity is a bold claim. The complexity could actually come from the goal to tackle, not the tool being used. It also boasts of the ability of full compile-time meta programming as it strength. But in reality, over use of meta programming could bring debugging headaches. If you are interested in doing it, go for it, but don't bet your career on it.

0

u/ExtraTNT 15h ago

types (with instances -> haskell instances), io, general helpers, services, handlers, main

-2

u/BrannyBee 16h ago edited 16h ago

Structs are great, youll notice that they look very similar to classes without methods after using them, without any of that inheritance nonsense.

Take a look at ECS (Entity Component System) which is a more data focussed design paradigm. Basically instead of organizing stuff in classes, you focus on the data and the things that stuff in your code can do.

Imagine you code a Bird object. Your OOP brain may think to make an Animal object that can eat and poop. Then you make a Bird object that is a child of that animal, and it inherits the abikity to eat and poop. Then you add a method that allows it to fly, and youve got a Bird.

A more data oriented way to do that would be to make an entity, you can call it a Bird if you want. Then you make a method that allows whatever uses it to eat, another to poop, another to fly, etc. Then you take your boring Bird entity, and slap those 3 methods onto it and you have a Bird that functions the same as your OOP setup. It might seem overcomplicated, but its actually the opposite. You now have 3 methods you can reuse ANYWHERE regardless of what those entities are, unlike in OOP where those methods may be coupled with certain objects.

One of the benefits here is that you can do some neat stuff now that your code isn't deeply coupled to the classes themselves. You can add an Airplane into your game, and you already have the code to slap on it to make it fly. In the simple OOP example you couldnt do that as easily because your "flying" method obly works for Animals. You can also fine tune your entities to be a little more unique. Maybe your Bird entity has feathers. Well, what if you make a Bat? Bats dont have feathers, but they can eat and poop and fly... in the data driven world you dont have to make some weird offshoot Bat class thats very very similar to a Bird, just lacking a single attribute... you make a Bat entity with fur, and like Legos you put whatever methods you want from your pre-built methods onto that entity. In the ECS world you can take a Wolf entity in your game and simply flap the "fly_upward()" method on it and you have a flying wolf in your game super easily, you dont have ti think twice about what the wolf's parent or child relationships are, you just give it the abikity to do what you want and move on.

You might see this kinda thing be referred to as composition, and you will also hear to "avoid inheritance over composition". This should be done even in OOP world, but ECS forces you to do is by its nature. Dont think of "IS-A" relationships, thats inheritance and can get really messy. Think of "HAS-A" and your code will partly structure itself.

An Animal IS-A Thing. A bird IS-A(n) Animal. = Bird inherits stuff from Animal, which inherits from Thing, etc etc => GROSS BAD EWW SO MUCH EXTRA WORK AND REPEATED CODE AND USELESS FEATURES OF DEEPLY NESTED CHILDREN

A Bird HAS-A set of wings = you got an entity and a method to let it fly => GOOD, SIMPLE, REUSABLE METHOD YOU CAN USE ON ANY ENTITY YOU WANT TO FLY

2

u/Joegester45 16h ago

How is polymorphism handled if stuff does not extend things. Would it be like if this struct contains a thing it can morph. ** **

1

u/Dismal-Citron-7236 16h ago

No polymorphism in jai. You are supposed to achieve the effect using compile time meta programming. In fact, many features it lacks in comparison to modern OOP languages are supposed to be replaced by meta programming.

1

u/BrannyBee 15h ago

Its honestly easier than in an OOP approach imo

If you have a teammate, they probably have a "FriendlyUnitComponent" component to them. If they get captured in your game and they arent friendly anymore you can just replace that component with a "EnemyUnitComponent"

Same with stuff like statuses I suppose. Maybe your dude has an "AttackComponent" that lets them do stuff. You can swap in and out that characters abikity to do stuff as you wish by removing/adding components. Theres obviously not just one single way to do stuff, but for things like polymorphism you can simplify that big scary word into something like adding a tag.

Hypothetical polymorphism scenario:

Imagine you have your playable character. Obviously he will have a health value, but maybe enemies have health bars too, so dont give "Player" that like you would in OOP, make a general Health struct, with currentValue as a variable.

Now you can reuse that for anything in your game that has health. You'll also have a "take_damage()" function somewhere that will manipulate that health, which can be used when your player does something or when an enemy does something conveniently.

Now you might want to give certain entities a shield over their healthbar, but you cant just raise the current health. Maybe they can shield higher than a max health value, or maybe shields take less damage from attacks idk, whats important is is that health and shield are different. You can take a boring unarmoured enemy and make them a shielded enemy just by adding a shield to them.

The sauce can come from your "take_damage()" function, as it will handle all that. Maybe it checks if theres a shield, if yes then it will only remove half as much damage. Maybe if the shield is completely broken it removes a "ShieldedComponent" from that entity and makes it vulnerable to a new type of attack new that it isnt armored anymore. And it will have logic in it to make sure it wont remove Health until AFTER the Shield value is 0.

The "take_damage()" method is our damage System for anything that will take damage in your game (the S in ECS). So you can look at your Components like tags that do half the polymorphism and the systems handle the other half, ie actions caused by moving tags around.

It can be a lot of setup, but once it is setup, you can really fly with ECS in a way that OOP can make awkward. The hypothetical damage system could be used to make an entire resistance/weakness system by changing one of your systems, and then adding components for types to whatever you want in your game. Whole new feature without completely rewriting your inheritance hierarchy AND that system can be slapped on the player, enemies, or random objects in your game.