Jump to content
  • Advertisement

Kirlim

Member
  • Content Count

    35
  • Joined

  • Last visited

Community Reputation

207 Neutral

About Kirlim

  • Rank
    Member

Personal Information

  • Interests
    Programming

Recent Profile Visitors

The recent visitors block is disabled and is not being shown to other users.

  1. I hope that I didn't answer in a way that made you feel like that. It looks like you've got a problem on architecture level, where all kind of opinions and alternatives are worthy considering. That being said, even though its a highly abstract choice, it will deeply impact the more concrete levels as you code. Choosing a bad architectural design might end up in technical debt or some good time of refactoring. Choosing a good one is hard because everything has drawbacks. When working with assumptions, it's hard to find a solid ground to evaluate any options, and an assumption can simply blind us to better approaches. For example, if all you need are boxes for raycasting, I'm pretty sure the bounding box classes already have raycasting on them implemented. If you have few enemies to scan (lets say, up to 100), then spatial partitioning might be meaningless. With a list of all enemies, you can iterate that much without ever noticing a cost (and you can distance as a culling method). The best solution is the simplest one that lets you get started, but we can only help on that as you cut off assumptions. For example, making hand-picked solutions might seem costly and time consuming, but maybe managing active colliders based on player position might be even more. There are tricky things like the cost of changing the static colliders of a scene, and other annoying things like having a code to detect which collider belongs to which tile, to then know which behaviors to activate in which situation (you get the idea). There is also the possibility of not using some features of your current tools, and add a few more to your game. Its perfectly fine on going with what seems fastest and easiest right now, too. Quickly validating game ideas is valuable. If you find a problem later on, you'll know for sure what you want to solve. It's a valid approach and tradeoff. Just be sure to code in a way that is easy to change. If you go with this approach, the suggestion is to "fail fast, fail often" to quickly react each iteration.
  2. Each tile needing it's own set of data does not imply that every tile should be a MonoBehavior, or even have it's own GameObject. As you mentioned, you can use a common class (or even struct, to enforce value type) on some type of organized containers. Then you can process it anytime and any way you need, without the memory overhead of managing lots of behaviors (or even the memory overhead). About the collision detection, I've got really bad experiences trying to make Unity's one working the way I wanted on 2D games. It felt like using Unity against itself. I've ended up rolling my own collisions systems (one was based on simple bounding box - unity already have most of the code for this, and other was a Sweep SAT). Took more time, but overall, felt better than fighting the engine - I believe it's collision detection is better fit for real time and/or realistic simulations. Are the colliders really needed, or that is an assumption? Assumptions will tend to lead to premature optimization or pessimization of something that you actually don't ever need! Since it is a turn-based rogue like, having colliders on map feels weird. Maybe there is a simpler solution, without troublesome collision systems?
  3. Kirlim

    Logic in ECS?

    I'm risking being out-of-topic here, and perhaps just saying something that you already known or others said. It is also, only a belief of mine and I have no data to back it up, but... When working with an ECS, it feels like data-oriented design fits better than thinking on components as instances of classes (with their own methods). By system, I understand as a set of controlled functionality with inputs and outputs. By manager, I understand as something that has to handle some kind of ownership. Using a data-oriented approach, your components will start becoming "tags" and data will end up mostly on systems or containers used by systems and managers. Since logic is a series of transformations over data, it is very likely that the best place to let it be is in the systems themselves. Of course, data-oriented approaches will have some degree of conflicts with Object Oriented Programming concepts, but if you're using an ECS, odds are that you're already having some attrition, anyway... So, answering "3) Extending upon 2, what really are Systems supposed to handle?": I believe that systems should handle most consistent data transformations that can be run batch.
  4. Thanks for the discussion! I liked the way that the type traits solution help me in expanding type information (if needed) without adding more template arguments. I got into a small problem with include hell so I left out the reference type for now (need to change some design to use it...), but I'm going with this solution. Thanks for the clear code example! The generic identifier in my case, is meant to be used over different components and systems on-demand, but on some it is possible to say up-front groups of id types that are unlikely to change (i.e RendererId, TextureId). For these, enum seemed attractive - but I decided to not use them in order to use the same code for situations where enums don't fit. It didn't come to me that the bigger context of how it'd be used would affect the suggestions. I'll keep that in mind for other questions :}
  5. Awesome advice! Clarity of intention was something that I hadn't considered, and it'll definitely help a lot - specially on the long run! The enum idea seems like a good catch. I'll check how it'll fit into the code!
  6. I'm working a lot with identifiers types to help me while debugging or managing collections of instances in my code. Previously, I had using directives as the following example code: using AnimalId = unsigned int; using TreeId = unsigned int; After a while, I accidentally mixed AnimalId and TreeId in my logic, so I've decided to use structs in order to make the identifiers strong typed. To avoid code duplications, I've created a template template <typename TypeTag, typename ValueType> struct IdType { ValueType value; // Methods for comparison and hashes }; However, I believe there's two different ways of using this template: // Using directive struct AnimalIdTag {} using AnimalId = IdType<AnimalIdTag, unsigned int>; // Inheritance struct TreeId : IdType<TreeId, unsigned int>; And here is where I got in doubt. It seems both ways are valid, but there should be some differences. For example, with inheritance I can forward declare the TreeId on headers, which doesn't seem really feasible (in a painless way) with AnimalId. However, TreeId uses inheritance, and my knowledge on how inheritance and templates works "in background" is too weak to say, but it feels like there might be some hidden drawback. Are there differences to make deciding which one to use easier? Or there's currently no drawbacks (besides being able to forward declare or not)?
  7. Kirlim

    Making an UI in c++

    Usually the requirements of in-game UI and tools UI are different (tools might need way more complex and elaborate views). If your usage example is exhaustive, you won't need a lib since rendering texts and rectangles is something your engine will need to be able to do anyway. A bit off-topic, but no IDE's in a team project of a game engine? After a few thousand of lines and dozens of files, this might slow down the team a lot - human memory is unreliable.
  8. Kirlim

    Game data container

    Usually too many dependencies, be it data or behaviors, are caused by classes that tries to do too much. Or, in some cases, that you need to pass data from class to class up a looooong stack of methods and functions. Either way, this might lead to code hard to maintain. Some other things that might help: Prefer composition over inheritance Stay alert for opportunities to use some type of event bus for communication and data retrieval (good alternative if you keep up references to data only to pool their state) Prefer horizontal relationships of classes, instead of vertical (classes that works together, but does not own or inherit from one another) Mitigate responsibilities and abstraction in a way that a big system can point you to smaller systems that point to even smaller systems. Pass the most abstract level that still makes sense around constructors. Avoid premature optimization. It might be the case that, trying to make things run fast, you made the code hard to work with even faster. Specialize. Specialize. Specialize. Classes that do less often have less dependencies and are easier to coordinate with other classes. SRP (Single Responsibility Principle) is powerful.
  9. Kirlim

    Asset storage

    Having recently (mostly) done something like prefabs loading myself, what I can say is: only you can decide. I'm assuming C++ because well, pointers (and I'd advise using references if the functions cannot work if they are null. One less NULL check, a smaller chance of headache seg fault). Personally, I'd say YAGNI. A RTTI system in c++ is overly complex, will rely on templates or out-of-compiler code management, or special compiler rules, or everything together in a painful mix that'll take ages to finish and have it working fine. You probably won't have the time, and this can kill your motivation. I'd stick - as I did - with serializing and deserializing functions. There's a chance you'll need or want something more flexible or with less boiler plate, later on. Only when you are sure that something will have a real, consistent, strong return of investment of your time, only then you refactor. This day, after all, might never come. But anyway, you can make your life easier. Use composition, inheritance or any other technique to isolate the serial code so that it'll be changeable with less effort if you ever choose to. If you never choose to change, you'll still have clean and testable code. On a personal note, having Save and Load methods for serialization can become a problem. If you want it to go both ways (read and write), I'd say to use only one function for both reading and writing. For example, you can have AbstractSerializer, which is implemented by JsonReadSerializer and JsonWriteSerializer. Only one function is half the effort, and it mitigates the risk of your Write and Read operations somehow getting out of sync about the order or size of data.
  10. Thank you for both responses! They helped me to decide more easily where to put those methods without going by "feelings" alone.
  11. I've recently started reading Clean Code: A handbook of Agile Software Craftsmanship. The concepts in there threw me in some occasional code refactoring. While making changes, I end up with methods that are entirely helper methods and does not make any sense in being exposed as public. I make them private. Some of these methods also does not need to access instance or class variables or methods even while still being tightly coupled to the class itself (i.e a helper method to ensure consistent text format related to the instance, for debug logging). I make them static. On the Header file, I declare those private static methods in the class declaration. They feel like useless noise, since they are so "private" and "coupled". In this situation, is there something that is believed to be a good way of action? I thought of moving them to anonymous namespaces into the source file as functions, but then it feels like I'm hiding functionalities.
  12. If you have nothing to prove it is and where it is slow, then it's not slow. Until you can prove it, at least. Except for on-the-spot optimizations and on-the-planning optimizations (which can easily become premature optimizations), nothing else should be optimized until your profiler DOES tell you that something is too slow. Instead, focus on features until your frame rate drops, or the project is on polishing phase. Fast code usually is dozens of times slower to change, and you'll need to change things a lot. This being said, it seems like you're not putting the loop thread to sleep. This means that the loop will run at 100% even if there is nothing to do, constantly checking if it should run a logic iteration and if should render. In other words, your task manager will show the executable as a beast even when it is actually using the processor to process nothing. Be aware that once you put your thread to sleep, it is the Operational System that will bring it back to life, but you won't have full control on "when" that'll be.
  13. I agree and think it is entirely true. Not exactly. For example, let's say you have a group of two classes that are quite often used together by end classes, say, NetworkStreamSerializer and NetworkSimulationCommandBuilder. At first, you might pass both of them as dependencies to classes like NetworkInputHandler and NetworkOutputHandler, but over time it might make more sense to refactor them both as used by a new NetworkCommandsSerializer. With that, you can hide them under a single class (less stuff to mock), and even try using a factory for both production and tests environment (there's a chance you'll need only one). This is just and example, but I can't think of a better one right now. Each case is a case. Service locators are used when you don't want to know where your "services" are, just want to access them. Wrapping classes is more like making a bigger, more abstract class with a wider (yet specific) responsibility, which you can pass down as dependency. Your dependencies will either be data or responsibility holders, so wrapping up many small responsibilities in a few, wider ones might actually make it easier to see how dependencies and business logic moves around. As final note, if you absolutely need to have so many dependencies being passed around that it hits your peace of mind, consider using a DI Container Framework and a mock framework, and let the tools do the magic. Quality is surely the most important of all, that's for sure. Specially in games, where a bug can make an incredible game die painfully. But unless you're going for experience alone, delivering is important as well... 
  14. Don't take an argument by a single example, nor a piece for the whole... By saying last option and to meditate, I meant it. Any type of globals and the likes comes with an awful lot of problems, being the difficulty of testing only a small share. But still, they won't render unit testing utterly meaningless, nor they are the only cause for bugs and crashes. Sometimes, when properly used, they can still be a solution that works better than others. As a side note, most well chosen globals are meaningless for unit tests. You wouldn't, for example, make a unit test to read the keyboard to execute an action. You'd most likely test the keyboard reader and the actions separately. Also, instancing whole systems or mocks that represents systems is also possible with DI, when things go wrong. If there were something such as the always correct way, programming would be a lot easier and boring...
  15. "One class has it all" is, as pointed out by kylotan, a good flag of bad design. Dependency injection usually results in huge dependencies lines naturally. Please do note, however, that no "100% this" solution is adequate. Sometimes, breaking DI in strategic points will actually make your code easier to change and understand. In the case of "one has it all", I've handled this problem in a few ways - Grouping dependencies into a single, container class. - Passing a "kind of a" service locator, usually in the form of std::function or class. - Removing dependencies. Some dependencies are there only for your convenience. Much more than you think. - When all else is panic, meditate on Singletons, Service Locators or Global Variables. This last item is a last resort. Singletons, Service Locators and Global Variables are all almost always an anti-pattern. But sometimes, you only need, say, one Keyboard Input Reader (or one mock of Keyboard Input Reader) accessible at a time. Passing this by parameters all the way down from the most abstract class of the engine down to your logic components will result in dependencies going over objects that don't even need them. It sux, and makes code evolving a  pain. If it is too generic, meditate. The bad solution might be, actually, good. (ps: I usually hide globals within an indirection, to help changing definitions when in tests environments. Downside is that I need someone to initialize them.)
  • Advertisement
×

Important Information

By using GameDev.net, you agree to our community Guidelines, Terms of Use, and Privacy Policy.

We are the game development community.

Whether you are an indie, hobbyist, AAA developer, or just trying to learn, GameDev.net is the place for you to learn, share, and connect with the games industry. Learn more About Us or sign up!

Sign me up!