Advertisement

Unreal Engine vs Unity Engine

Started by March 15, 2017 06:42 PM
80 comments, last by Dirk Gregorius 7 years, 7 months ago

To be fair, the issue of unwanted Updates goes away with a simple boolean (e.g. UE4's bCanEverTick)

The issue of moving the behaviour into systems rather than game objects is a commonly-discussed one these days. My experience is that the approach is subject to a fair bit of confirmation bias, where people easily see the benefits (more cohesive code within a single feature) and tend to overlook the downsides (increased complexity wherever features interact). Obviously there's benefit to having composable code components, but I'm not convinced we've really found an effective way to decompose game objects yet that doesn't involve a significant amount of Kool-Aid drinking.

To be fair, the issue of unwanted Updates goes away with a simple boolean (e.g. UE4's bCanEverTick)


Does UE4 keep an "update list" for tickable objects and only put the objects that have bCanEverTick true into it? Or does it just check the boolean every frame? The former is actually just a simple form of what I'm describing here. A general principle here is to sort your objects by how they're going to act.

increased complexity wherever features interact


I have yet to see this happen, at least to the point where it would be offputting or difficult to deal with. Do you have an example in mind?

Obviously there's benefit to having composable code components, but I'm not convinced we've really found an effective way to decompose game objects yet that doesn't involve a significant amount of Kool-Aid drinking.


I just gave you one - aggregations of data should be sorted accorded to purpose and lifetime. What's "Kool-Aid" about this idea?

To be clear, I'm not advocating a component-entity-system architecture - that would be the extreme and dogmatic form of what I'm talking about.
Advertisement

Unity's choice to put a transform in their base class is obviously a breach of data normalisation rules, so it's a diry hack too... But it's 'useful' for 99% of their components so you can understand why they chose wrong-but-works over pure-but-annoying.


I'd have to double check the code (although I was looking at it just the other day so I'm 99% sure the following is correct), but that isn't quite how things are done; while it's true created game objects all have a 'transform' that is in fact a component which is added to the gameobject at construction time.

Unity's choice to put a transform in their base class is obviously a breach of data normalisation rules, so it's a diry hack too... But it's 'useful' for 99% of their components so you can understand why they chose wrong-but-works over pure-but-annoying.


I'd have to double check the code (although I was looking at it just the other day so I'm 99% sure the following is correct), but that isn't quite how things are done; while it's true created game objects all have a 'transform' that is in fact a component which is added to the gameobject at construction time.


Right, yeah. Transform is just a component.

I have yet to see this happen, at least to the point where it would be offputting or difficult to deal with. Do you have an example in mind?

You seem to forget what makes game engines different from other software: There will be, people with only basic programming knowledge working with the code.

Even if some thing is considered to be only slightly complex to a programmer, it will be labyrinth for the rest of the team.

The reason Unity and Unreal work as they do, is the same reason most software uses Ctrl-Z as undo; it's what people are use to.

Right, yeah. Transform is just a component.


Transform is a component, and an instance of the component is part of the GameObject base class. Any GameObject comes with a Transform component pre-installed.

I'm going off-topic to nitpick, sorry


I agree with you.

As you wrote, they are well understood and despite their problems they mostly work okay.

As you mention, in principle you can implement the functionality of a game object in other ways, such as providing your own function pointers for whatever functionality you want. That would offer several benefits. That is more common with plugin architectures, or in languages like JavaScript or Python or Perl, where it doesn't matter what the things are called, all that matters is that you have something that looks right and matches all the slots and tabs as parameters or data, or that can be queried to give things that match the requirements. But since "easy" and "familiar" often win over "pure", the solutions the big engines offer really avoid the biggest problem.

On the virtual function pattern, that's a fun one. In some ways it is like a sharp knife, the young'ins always end up getting cut a few times until they figure out how to avoid the sharp parts.

Virtual functions are an amazing solution when the problem is that you need to vary behavior based on the concrete type. That is, if you are calling and doing different functionality for each thing it is extremely difficult to beat . Unfortunately they are a terrible solution when that behavior is no functionality. The "do nothing" virtual function is a cost that accomplishes nothing. Good code bases address this. Poor code bases make everything virtual, or in Java, never make anything final.

Since we're in the lounge and we can ramble, those of us with years of experience know quite well just how much those virtual functions can harm performance. Calling thousands of virtual functions every frame that do nothing is a terrible waste. They have a cost to look up the proper function, then the cost of a call, then the cost of returning. It may only be ten or twenty nanoseconds per call, but multiplied by many thousands you can easily reach microseconds of wasted time.

There are a few good and common improvements. Unity uses one common pattern and Unreal picked another.

Unreal chose the pattern to have a flag in the base class that tests if the function should be called, then the cost is only paid if the flag is set. Since it is in the base the variable is inlined and looks at the data bundle that is probably already on the cache with the object. That improves the time to a fraction of a nanosecond if the call doesn't exist, plus the full cost if it does. However, it still has the cost of doing all those tests.

Unity uses the pattern to scan early (at load time) to see if the functions exist, then only call the functions where it exists by registering them and going through the registered items. It gets the improvement that unimplemented functions aren't called. The drawback is that if implemented it is always called, an active object cannot de-register for updates. So it avoids the problem of the additional tests like Unreal's model (yay!) but cannot readily be removed from the list if there is nothing to do (boo!).

The best systems I'm aware of for those are for the things that need it to register that they need to be called, and are removed when they can be stopped. The collections of functions can all be called as tasks. started in parallel if the system is up for that. All the functions are indirect calls rather than virtual dispatch, and there is no cost to test the status. Items can be removed when they're done, added back on if that becomes necessary. You only pay the cost for work you actually need done, and everybody is a winner.

But sadly, those systems have some added complexity and are slightly more difficult to use correctly, so we're back to the earlier "easier" patterns of using comfortable, familiar names, along with their associated costs.

Advertisement
In Source we use something similar to what you describe. An entity can register thinker functions. They actually are only called once. So if you want another call you need to re-register. The other nice feature is that you can pass a time stamp. So instead of being called per frame, you can be called maybe every 5 ms (which might be an appropriate interval to update your sepcific entity). I really like that approach.

https://developer.valvesoftware.com/wiki/Dota_2_Workshop_Tools/Scripting/ThinkerFunctions
Unreal chose the pattern to have a flag in the base class that tests if the function should be called, then the cost is only paid if the flag is set. Since it is in the base the variable is inlined and looks at the data bundle that is probably already on the cache with the object. That improves the time to a fraction of a nanosecond if the call doesn't exist, plus the full cost if it does. However, it still has the cost of doing all those tests.

Eh, I'm pretty sure Unreal doesn't do those check regularly. There's the "bCanEverTick" variable (alongside a few others), and I imagine if CanEverTick is false on initialisation, then it isn't inserted in some kind of tickable-list. Hard to find exact details, but seems they have a system for registering tick-functions:

https://answers.unrealengine.com/questions/499858/are-tickers-multithreaded.html

So at least from a performance-standpoint, shouldn't have any overhead, minus the function being virtual vs. being registered as a nonvirtual function pointer.

increased complexity wherever features interact


I have yet to see this happen, at least to the point where it would be offputting or difficult to deal with. Do you have an example in mind?


I see it in every single component-based project. The facts are these:
  • It's not possible to have a standard decomposition of 'game objects' that works for all games, or they would come as standard in the engine. So we write our own;
  • These divisions of responsibility are almost always slightly flawed, so there is interaction between them;
  • These interactions across components are necessarily more complex than if they were within the same object, because several invariants are harder to enforce (such as ensuring that both components are present, that execution happens in the order you expect)
I've seen several different ways of trying to solve these problems, and they're all solvable, at the expense of moving the complexity elsewhere.

Obviously there's benefit to having composable code components, but I'm not convinced we've really found an effective way to decompose game objects yet that doesn't involve a significant amount of Kool-Aid drinking.


I just gave you one - aggregations of data should be sorted accorded to purpose and lifetime. What's "Kool-Aid" about this idea?


Nothing's wrong with the idea. It's perfect, in principle. In practice, I've never seen a good decomposition along those lines.

I also disagree that it should be done along the lines of data rather than behaviour, but hey, I think we've talked about this before and are unlikely to agree.


To be clear, I'm not advocating a component-entity-system architecture - that would be the extreme and dogmatic form of what I'm talking about.


Doesn't stop a ton of newbies trying it based on some shitty blog though. :)
You're right, we have discussed this before, though I don't recall the exact thread. As before, I feel like you're still conflating what I'm talking about with CES and object composition in general. I expect some non-commenting readers probably do, as well, so I'm going to clarify: it's not CES, and it's not "composition" in the sense that is usually meant. There are crucial differences here.

The way I've seen CES typically done, you might have a "MovementComponent" that stores a player's velocity and is responsible for updating it, and maybe calculating accelerations and things, too. This MovementComponent would exist for the entire lifetime of the object. A developer could call some Unity-like "GetComponent" method on the game object to grab the MovementComponent and manipulate it. The component would essentially be part of the object's interface. This is not too far different from just making the MovementComponent part of the object itself in the code as well as conceptually; in fact, the component may have started out as a velocity field in the original GameObject, if the new architecture was refactored out of an old one!

The kind of architecture I'm talking about would do this with a blob of state with a velocity, that can exist per object, but it is not a "component." It is not a "thing" in and of itself; it isn't even an object, really. It definitely isn't a part of the object's interface. It barely qualifies as a part of the object's state, actually. You can think of it as a message ("move this object with this velocity") that can persist over multiple frames, or that re-sends itself after processing until some condition is reached. Or a "token" that indicates that an object belongs in a particular set that needs to be processed in a certain way, and it happens that it's convenient to use the "token" as a way to bind state to the object for the purposes of that processing. If you prefer to split these things along behaviour - though I would argue that splitting along behaviour and splitting along data are usually the same thing - you can do this for that, too - instead of switching on a flag or calling a virtual function on the object to determine what to do with it, just put the object - or more likely, a reference to the object - in a list of objects that need to be processed in that particular way, without any other state.

These "state tokens" are fundamentally ephemeral. When the object is no longer in that state, all the data it needed for that state goes away. You no longer have to think about it; you can no longer access it, so you can't accidentally screw things up and put the object in a bad state. Furthermore, hopefully you're doing this through some kind of class interface, so you have a place to put a breakpoint to see when an object is getting put into a state that it shouldn't be.

This is sort of analogous to how tokens and counters can be used in tabletop games. In a tabletop 4X game, for instance, you might send a fleet of spaceships to attack your opponent's colony. Now, you could have a single miniature on the game board that represents the fleet. You might keep track of how many ships are in the fleet and how much health each ship has using pen and paper - which is a lot of bookkeeping. Or, you could just have a stack of counters underneath the miniature representing ships and place damage counters beside the miniature when a ship takes damage. Then when a ship repairs itself you just remove the damage token; when a ship dies, you remove the counter that represents it in the fleet. Same idea here, but with pieces of state in software instead of cardboard counters.

This is getting very off topic, though. :P

This topic is closed to new replies.

Advertisement