Taking Roaming Ralph for example - it’s about 300 lines contained (mostly) in 2 methods. There is also input, camera, scene setup, physics, collision, animation and game logic all mixed together.
Where I’d like to go personally:
These things /could/ all be part of their own sample:
Ralph can import that logic. Likewise we ‘could’ add a few more samples that are much more advanced than Ralph without duplicating any of that code. Expanding the official sample library while not adding much / if any footprint to total # lines.
Updates! Weekend is approaching so I should have some time to get work done on samples. Right now as I get some free time working on design.
Per the earlier discussion on standards and how to identify if a sample is ‘good enough’ I thought useful to outline some guidelines I’d like to use. Style guidelines are important as well (ie camelCase vs snake_case) but I think architectural principles as well would be great to go by.
One that I feel, pretty universal to code structure/quality is SOLID Which is fairly basic stuff, but essentially says:
Every entity in your software (usually talking about classes here, but can also be applied to methods as well) should have a single function, and there is only one reason to change that element.
How it translates to Panda code - if you see monstrous classes that do too many things, the same for methods - say mixing collision, physics, lighting, shader setup, this is an immediate signal this rule is being violated. Its OK for very small use cases (a single page of code or so). But getting into more than that this starts to come into play.
Software entities should be open for extension, closed for modification. This essentially means, that code when written well - can be ‘modified’ through techniques like inheritance (although there are many other methods as well), obviating the need for folks to have to ‘modify’ the code of a generic component directly.
How this applies to Panda samples - Lots of our samples are unnecessarily complex / overly bloated because too much is being done in a main application class. It can give the illusion that Panda is too hard to use, when its just bad code organization. Some of that bloat can be moved into a non-application class, and then that class imported. What you would see is a sample that contains code ‘only’ for what that sample was intended for. (For example, you wouldn’t want a ‘networking’ sample app to be 90% shader setup code. Better to import that shader setup, customize it using this principle - and the user can focus on networking.)
(L)iskov substitution principle:
This one sounds complex, but it means that components should be loosely coupled. In the sense if you have say a Physics class, and derive a Bullet class, both can be used interchangeably in software that wants physics, without knowing if it really is the ‘Bullet’ library or another. It also allows you to swap out / replace modules in your software without changing much other than the module swapped.
How it might apply to Panda - I already gave an example here with Physics and Bullet. But say a user sees a sample and wants to remove ‘Bullet’ physics and use another library. If you see code that is Bullet-specific in the sample, now they’re not able to do that swap, and that code sample is less universal, less applicable to other game types, not great design. Lots of Panda sample code has this type of problem.
(I)nterface segregation principle:
This is closely related to the first principle (Single use) but it applies to interfaces, or base classes that are intended to be extended. You don’t want to force users to write a lot of code to implement an interface / derive a class. Better to split these into multiple pieces so that the user can choose what to implement as needed.
How it could apply to Panda - in Python an interface is just a ‘class’. So this applies to when making re-usable classes that samples might derive from, you don’t want those components to be too big / have too many methods. Since very little code is re-used today in samples this is more for future work.
The big ‘code smell’ here, is if you see a class that ‘must’ be derived for it to be used, but it forces you to create all sorts of empty methods you don’t need. This is a sign there might be improvements to make here.
(D) Dependancy inversion:
This principle essentially means loose coupling. What is says is that if you want modules of your code to be re-usable - try not to tie them together too tightly. This can help improve things in many ways - for example. Say you want your Physics library to notify objects when there is a collision. One way is to have a Physics class call a gameplay Entity method ‘oncollide’. But you’ve now coupled your Physics library to gameplay (bad idea). Better to have Physics module provide some sort of ‘PhysicsListener’. Then, your gameplay code can derive from that (inverting the dependency) and removing the tight coupling.
SOLID shouldn’t be taken too literally - I prefer to think of the logic ‘behind’ each principle and apply them universally to classes, methods, modules, libraries. Heck some of the principles even apply to mechanical engineering and UI design!
It’s also a kind of skill. Seems complex at first - especially for the code /designer/, but when folks /use/ the code they might not understand why it was designed that way, but you’ll hear from users ‘this API is really easy to use’ or ‘it just makes sense’. Which is what I’d like users of samples to say!
In the case of Roaming Ralph specifically, how many of those things really justify or are suited to becoming separate modules?
The camera is very simple–basically just attached to Ralph
Input processing is also done in a basic manner, something far too simple for a separate module, I feel.
Animation likewise is handled very simply–there’s no FSM in place at all, for one thing.
Collision seems to me to be too specific to a given project to be suited to separating into a module, or at least any more than it already is.
Now, we could re-engineer Ralph to use more-complex, module-based versions of these things. But I’m not sure that this is wise: Ralph is, I presume, intended to show a simple example of working with the engine. Making it more complex seems to me to make it more intimidating, while I fear that hiding logic behind other samples makes it less clear.
We can have complex examples, but I don’t think that Ralph is a good candidate for that.
You note that Ralph is 300 lines long. But looking at the code, it seems to me that a good chunk of that comes from comments and empty lines–and even with them, it honestly feels short to me.
Of the principles that you outlined there, I’m inclined in this case to agree with (O): I do think that reworking our samples to make better use of classes and methods seems wise.
Regarding (L), it would seem that you’re talking about rewriting chunks of the engine, which is perhaps a bit out of scope for the samples. ^^;
Perhaps I’m missing something here, but in Python, wouldn’t one generally give the base-class empty methods, thus allowing the user to just override whatever they specifically want to? o_0
But perhaps there’s some way to do C+±style virtual methods, possibly using decorators, that I’m not aware of!
… I’m not sure of why one would want to do that, however. A stub-method seems both simpler and more sub-classing-friendly. o_0
For the most part, I’m not convinced that it’s all that important to stick with this one specific philosophy of design.
Honestly, from a game-development perspective, this seems like overcomplication to me. It seems both simpler and easier–and thus more maintainable and reliable–to use the former route.
Plus there’s the fact that, as a game developer, I’m using an engine in order to focus on developing the game game itself, in general. I’d rather take the simpler route here and actually get on with the game! ^^;
This is a great question! I’d love to have your feedback on this one. Let me phrase your question a bit differently (if you don’t mind?) and then we can come back to Ralph.
Hypothetically speaking, ‘if’ we could modify the sample to be easier to understand, while having the same (or less lines of code) - but more modularity (ie add classes, or methods). Would that be a good thing? ie, warrant a valid change to the code?
Yes agreed, typically the engine would have classes that need to apply most of the SOLID principles. We don’t want users to build APIs / but use them, agreed here.
The only place these start to apply in samples are twofold: 1) Sample gets big enough / has enough code in it that one needs to start to modularize 2) Being a sample, at times you want to put ‘extra effort’ into modularity and cleanliness - it makes things easier to understand and also easier to re-use.
It does get back to what the purpose of a sample is - one perspective is that they should have ‘minimal code to make something work’ the other ‘show clearly how to do something, and allow user to re-use it’. I really prefer to lean to the 2nd type. (This is personal preference). So I’d be more inclined to apply SOLID to smaller, simpler code just to increase readability and improve re-usability.
Yes you’re correct! Normally interfaces are a bunch of empty methods. Especially in Python. The point here is - lets say that there are 100 empty methods that do 10 different things, this is saying, you probably have a class that’s too bloated, maybe that should be 10 interfaces with 10 methods that do 1 function each. It helps you decide how modular you need to go.
Yes applying SOLID blindly is normally never done - they’re like guidelines to frame how you think about code. ie. some might say you ‘should’ exercise 20 minutes a day - but in reality, its ok to do 30. Or an hour every other day. Or just walk to work… etc. Generally people understand the principles behind the thing but use it when needed.
You mean you’d prefer Physics->Entity framework to be coupled? Rather than Physics to standalone as a module? Just want to make sure I understand the point.
Agreed - in essence that’s the idea. Cleaner, better, simpler samples help you focus on just getting the game done. A lot of the ‘extra work’ here mostly affects the person who is writing the API, or the game sample, to make things more clear and re-usable. The game developer doesn’t need to know or be aware that SOLID even exists, they just find it faster/easier to do the task.
As always, really appreciate your feedback @Thaumaturge ! I feel bad to take so much of your time for all this, but it’s very appreciated. The discussions are really great. Thanks =)
Why do we need complexity with modularity and other things? We have an example of a step-by-step development: the solar system. You can just break the Ralph into stages. Window, Input, Collision, Camera, and so on.
Agreed! The solar system is a great example of how one could use multiple ‘modules’ in a single sample (hence a form of modularity). I would personally take a small step further and have those modules/stages actually all stand alone with a specific feature in each, and they can be imported into the application Module, for even more clarity and reuse.
The samples so far generally try to demonstrate one specific concept, eg. playing an animation, executing an interval, enabling shadows.
As such, I’ve even begun to think that it may have been a mistake to write the Hello World sample in the manual and the sample programs using classes that inherit from ShowBase, rather than some imperative class-less code. It just adds noise to understand what’s going on, and doesn’t really do anything to teach people OOP either, it just makes people think that they are required to do things that way, which isn’t so.
As for the more complex samples (like Asteroids and Roaming Ralph), it may be good to have some more structure in there. If the point of Roaming Ralph is to show a more complete walking simulation, then I could see a point to restructuring it on a better architecture. In practice, many people end up copy-pasting Roaming Ralph and building their first game on top of it, so there’s a good argument to be made that it is built on a stronger foundation. So, changes like splitting out the player logic into a character FSM would be a change I’d welcome. (So I guess there is a clear distinction between these types of samples.)
For what it’s worth, people (including yours truly) occasionally participate in game jams and do create some cool projects that could serve as an interesting demo, with some cleanup. Some of them end up here. The style of them is generally not great due to the strict time constraint in which jam games are made, so this may is a non-starter, but at least for my entries I would be fine if people salvaged it for parts.
I would say that if the modularity didn’t run counter to making it easier understand, and wasn’t over-complex for the size of the project, then–presuming nothing that I’ve missed–that would seem like a good idea to me.
That said, the main point, for me, would be showing the ease of use and capabilities of Panda, not in showing modularity itself.
(For one thing, there are people who don’t like leaning heavily into the OO paradigm, and that seems okay to me.)
That’s very fair, I do think.
I am very cautious about making particularly big samples. Samples, I feel, are perhaps best when they’re short, focussed, and elegant; when they show a limited number of things, and do so well.
And further, from a learning perspective, having to refer to an external module to determine how something works makes it more complex, not less, I would say.
The multi-level showcase that has been proposed is another matter, I feel–that’s less something that I would expect users to learn from than something to show people what the engine is capable of. It’s about looking good, not teaching well.
I would think that this might depend on the case. Sometimes it’s more effective to go for a less-elegant approach; sometimes sticking to a given coding philosophy can actually lead one to over-engineering something that could be simple, if a little ugly.
Thinking about it again, I think that it’s more that I prefer the approach that works best for the situation.
For example, look at Roaming Ralph: Making a whole separate physics-listener for the very simple setup that it has would just be overcomplication, I feel.
However for a AAA project that passes through hundreds of hands, having a strict, clear, separable approach might be very wise indeed.
For my own projects, I’ve never felt a problem with just having physics events call game logic, as far as I recall.
Regarding the API, I think that we should perhaps assume that it isn’t going to change much–at least within the frame of this push to make samples. After all, asking the Panda devs to rewrite significant chunks is perhaps asking a bit much if there isn’t a compelling reason to do so–especially when there seem to be plenty of other tasks to be done in maintaining and developing the engine!
The samples are another matter–but see my feelings above about simplicity in them.
Don’t worry about it: if I feel that my time is being over-used, then I’ll likely bow out of some or all of the discussion.
Agreed! This is exactly the rule of thumb that I try to apply to ‘all’ code that I write. If I can make it easier to read, and simplify (perhaps by applying SOLID or just some common sense) at the same time then it’s always a win. Looking at some code like Ralph, I see the same opportunities.
A nice side benefit of this too, that tidy code is also more re-usable, in a sample that’s almost always what we want, the concepts to be re-used by the game developer. (Either by copy/paste or some other method). We also want the developer to feel like the code is easy to adapt for their own games.
For sure, key here is not to do OO for the sake of OO. But ‘write high quality code’. Python has many other applicable techniques other than OO to use for modularity and reuse. For sure.
And modularity helps here - I could see some extremely advanced samples being possible that are also small/elegant with code structured the right way. In fact I think this is the main reason I personally like to work with well-written code. I can do more with less effort on my part, and more quickly. At the end of the day its how quickly I can get my game done (and with what quality) that is the measure of how good the code is that I’m using.
I feel this is a balance - it goes back to the S in ‘SOLID’ - if one module is doing too many things it can be overly confusing. I like to look at a screen of code that’s doing one focused thing, not have to struggle to understand the complexity I’m looking at.
Agree, safe to assume here that the engine API itself is out of scope for this effort. This entire discussion is really just about sample code and how that’s structured.
Interestingly enough though - when a sample is sufficiently complex enough to warrant ‘import’ of some sample code, you start to blur the line - once you’re making re-usable code you’re also starting to create a little API of your own. So it’s always good to be aware of principles like SOLID because they help everyone structure their own code - that is if modularity and reusability is important in their game.
My own opinion here. I’m a very lazy developer. In the sense - I want to write less code that does more. Less code, less bugs, faster features, easier to understand. And I’ve written many, many games over the years, who knows, maybe in the tens of millions of lines of code or more. I constantly think ‘if I had made my own code more re-usable I wouldn’t have to be writing this darn thing again’. I don’t know how many quadtrees, octrees, a-star, UI, AI, geometry intersection routines I’ve had to write from scratch just because the previous implementation wasn’t reusable enough / became unmaintainable.
Anyhow, that was a big tangent. In short I have learned over the years to think - all my code is potentially an API for a future project =)
I think that this is somewhat missing my point. My point is less to reduce the number of lines of code in the sample itself, but rather to keep the whole sample–including any modules–small and simple and elegant.
Rather, I would say that having external modules at all increases the mental load a bit and incurs breaks in the flow of learning, making it a little harder to take in what’s being taught. Having everything in one place–even if broken up into classes–seems to me to be likely to make for better teaching material.
To my mind that’s perhaps better handled by keeping the samples very simple: instead of having samples that show complex things, just have more samples that show simple things each.
How wise is this, I wonder?
I hear you–I’m just not sure that samples, directed at teaching users very new to Panda the basics of its use and showing some of what it can do, are the best place for such an approach.
In a showcase program? Sure! In code-snippets? Sure!
But I’m wary of doing too much of that in samples.
I do agree with this, completely. I think it’s better to be more explicit - let me try to rephrase:
Say Panda’s sample repository has, for example, 10k lines of code. If code is written poorly, it might be those 10k lines shows some simple ‘Hello World’ samples with no fully functional /actual/ game examples.
While, when written properly, that /same/ 10k lines of code could contain an entire 1st person shooter, maybe a 3rd person platformer game, in addition to ‘Hello World’ and all those basic samples as well. (Not by any form of magic, just by clean code structure and minimizing duplicates).
Given that there is overall the same lines of code, the 2nd scenario is likely better. For the user as well, there is less code overall to understand or read, and they gain more advanced game samples to work with as well.
The 1st case may have ‘less modules being imported’ in each sample but that actually results in ‘more overall code for the whole sample’ and by extension ‘feel less elegant’ to the user.
Completely agree for the simpler cases - ie, the ‘Hello World’ style things. (Or ‘Hello Networking’, etc.) And these are absolutely critical to have, people need these for learning. If we were to break up ‘Hello World’ style samples, it’d be nigh senseless and to little benefit, for sure.
Agreed!! One tiny step further that’d I’d advocate for, is for some of the more complex samples to leverage those building blocks as well, without duping that code. This is what allows to get to that next level of functionality / complexity but barely adding any code at all to what’s already there.
Writing re-usable code I’d recommend, strongly. For sure. (my calling that an API, might be just an opinion, of course).
I think where we might differ in perspective here is our definition of ‘sample’.
Let me explain - Panda being a game engine, has ‘samples’ that come with it by default. Now many of these (existing) ‘samples’ currently showcase things like rendering. I’d argue that nothing in the (current) source tree itself is actually a ‘real game sample’. Which brings the conundrum - a game engine without samples of games included. Now I can understand why this might be, for sure, as it can be intimidating using the wrong approach, hence why I think this design work is so important! With some strong fundamentals we can fix this gap and even clean up the code we already have, win-win.
Really appreciate the thoughts here @Thaumaturge . I think these are really great ideas.
I came across this while browsing the forum. I have not tried it myself, but it’s another example of a character controller “pip import style” from a member here: panda3d-character-controller · PyPI
I certainly wouldn’t complain if somebody built a more-comprehensive system that would be used in conjunction with the core Panda3D engine, in the same way that we have examples of now (Python packages). A system which would allow a more distributed development standard environment for people looking to build large projects with the engine. I’m not sure what this would look like honestly, it is not my interest area.
I don’t think the average indie solo dev is having Agile meetings, but such an augmentation, like any other package such as Numpy, may be of interest to advanced teams. Though, the interest will be limited I imagine, and at a somewhat high development time cost. It would be an effort to replicate what other, much larger engines are optimized for, and taking at least somebody’s talent away from more practical nuts and bolts development like Vulkan support or any of the, officially recognized open issues.
You seem to be conflating “writing poorly” with “writing in a non-reusable manner”, which I’m not sure is accurate in the case of a sample repository.
In fact, one thing that comes to mind is that we don’t know which sample a user might pick up to start with.
Given this, it seems wise to have the full code for the sample available immediately, in the sample itself, or in at most one or two side-files contained within the sample’s folder.
But should a set of samples have full games like that?
Again, my feeling at the moment is that samples should perhaps be kept very simple. Show off one simple thing in each, without attempting to build a full game.
More-complex projects can perhaps be kept for showcase games, instead.
In the general case, in which one is writing for a non-didactic project, I would likely agree.
I’m just not convinced that making a such a set of modules when specifically aiming to create a set of samples is quite as wise.
Indeed, I think that we are looking at the samples in rather different ways!
To my mind, the samples are primarily didactic: they exist to show the user some of what the engine can do, and how certain simple things can be done.
Showcase-games can exist separately, and, not having the requirement of teaching, be more complex without the worry of how the code will be read.
I feel like more-complex teachings might be better conveyed via tutorials, in which explicit instruction can accompany the complexity of the lessons. (But then I might be biased in this, given my own tutorial.)
I will say that such a number might be more achievable than one might think; lines of code can add up quickly!
Consider: Let’s say that it takes about five seconds on average to write a single line of code. Let’s further presume that, in a given eight-hour day, around two hours are given to thinking, and a further two are given to lunch and various other activities.
Thus, in a single day, one might write 5*60*60*4 lines of code, equalling 72 000 lines.
Do you mean something like Ursina–a framework built atop Panda3D? Or am I misunderstanding? ^^;
Likewise, I don’t use it myself, but I see no problem at all with someone building such a framework atop Panda in that way. Indeed, I can see such a thing being useful for those who want a framework that’s streamlined in that way!
There may be something to be said for intentionally constraining one or more of the new sample programs to a very small size. It is logically possible to build a deep game, but without the high-poly skinned meshes, and the other things that bloat a program. The complexity instead goes into level design, showing state changes from the beginning to the end of a logically complete game.
We could write something akin to an old Atari game, but modern enough to actually run. As an example. Not necessarily arguing for a “retro” game, just suggesting the possibility of building really small, deep modern games in the engine.
It could also be a way to circumvent the “art problem” and show mathematically impressive scenes with a minimum of actual artwork. A procedural generation algorithm, for instance, may look impressive in wireframe.