• Advertisement
Sign in to follow this  

Unity Architectural musings

Recommended Posts

I originally posted this in another game development community and was largely met with confusion.

I will attempt to re-state my case here in hopes that (A) I can get a sanity check on the design principles and (B) I can have a more productive discussion about the design itself.


First, a motivating example.

Suppose we have a game in which there are cameras. A camera consists of a Position and a FocalPoint, both 3-coordinate vectors. For simplicity we'll ignore other attributes.

Now, the game begins creation when there is only need for one camera and one controlling mechanism: player input. But as time goes on, as always happens, the design is iterated upon and more features become desired.

All told, we will end our game's journey with several things wanting to control cameras:
  • Player inputs
  • Networked input (spectator cam)
  • Game replays
  • Pre-scripted cutscenes
  • Dynamic "temporary" cameras that do things like zoom to a target region, show it briefly, then return to player control
In most game engines I've ever seen, the camera object would have some interface like "SetPosition" and "SetFocalPoint" and all of these modules would then take a dependency on the camera.

My proposal is simple: apply some dependency inversion and wrap the notion of a "vector" inside a notion of a "vector source."

Instead of player input, netcode, replays, and cutscenes all pushing state into the camera, they instead hand the camera a "vector source" and say "ask me for your position and focal point whenever you need to."

The bottom line is that suddenly we have no dependencies between modules. Camera controllers can publish their stream of data anywhere - including into a buffer for replays or debugging - and the camera itself only needs to depend on a single interface.


If you're with me thus far, permit me to take this design a step further.

Any time you have a stream of data that changes as the game plays, apply this dependency inversion. Instead of having a push-into mechanism for propagating new state through the game, favor a pull model.

This does not need to imply a performance penalty; for example the camera module could update its position once per frame (by querying its source) and use an internally cached copy of the vector do set up view matrices etc. So really it's about the same as having external code push the value into the camera, just cleaner.


My hypothesis is that uniformly supplying runtime data in this way would make certain things much easier.

Consider the camera above. Once we have a position controller that works on the network, we can apply that vector source to any game object and it suddenly has "free" replication capabilities.

Or think about the way replays work. Any value in the game can be streamed into a replay file with uniform logic; just write an entry to a log every time the value source changes its state. To "replay" the log, just attach a value source to the relevant game object's properties, and the game will suddenly be driven by a replay instead of live data. No extra infrastructure or hard-coded logic necessary.

Now think about debugging. If a stream of values looks wrong, dump it to disk. Or plot it on a graph in UI. Or whatever. But you only have to write the debug monitoring code once. Any other compatible set of data (e.g. another vector-3) can be dropped in without any fuss.



I feel like I've been doing a terrifically poor job explaining this, because so far the biggest reaction I get from people is being offended that I'd suggest departing from ECS or whatever holy architecture is sexy to them.

I personally think it's not much more than a judicious application of age-old design principles (dependency inversion and abstraction) but it feels like an interesting way to make a game.


For the record, I have worked on games that did this (at my urging) and built a few tech demos that were designed entirely this way. I'm pretty happy with it as a strategy but I'm also very motivated to understand the outright hostility the idea gets from others.

Share this post


Link to post
Share on other sites
Advertisement

I am in favor of update-and-push for reasons related to control.

The request-when-needed model at this fine of a level has a few problems.

  • If the source hasn't been updated then you just get the same value back anyway.
  • If the value is dynamic enough that every request to get an update results in a new value being returned, then you have a performance problem and potentially a sync problem.
    • If only a single source is polling the "vector source" then you have exactly the same relationship as if a camera controller were updating a local once and sending that as the reply, and in this case you could just have the camera controller update "its" vector directly on the camera.
    • If many sources are trying to poll the "vector source" then you would want to have a mutable copy to send out for each matching request rather than updating it each time.
      • Now how often the camera controller updates needs to be tightly controlled to ensure consistent results.  Each pull should return the same result for the duration of a logical update, or for a single render, etc.
        • By the time you have gotten this far, the relationship between the camera and the camera controller isn't really, "I, the Camera, call upon the vector source to signal to the Camera Controller to update and give me a value."  The Camera Controller is updating at its fixed rate, buffering the value, and allowing things to read it when they need, which is how most of everything works.  It's exactly the same functionally and performance-wise as the Camera Controller updating its value and writing it to all the Cameras that need it (which again is almost always just 1).  Pull requests will keep getting the same value until the Camera Controller updates, so you are back to the first point in this bullet list.

 

Things have to be tightly controlled for results consistent not just between replays but even inside a frame.  You can't have 2 objects poll the time in a frame and get slightly different results or one will end up falling farther than the other.

 

If the main concern is really about relationships and what knows about what, I implemented a solution for exactly this inside my animations.
A Track knows only what floats or ints or bools are.  It doesn't know about vectors or any other types of objects.
You pass it a pointer to the float, bool, or int that it is supposed to update, the Track determines values as it interpolates between key values as you update it each frame, and it updates the value to which the pointer points.

To update a vector, you have 3 tracks pointing to 3 floats.  You can have as many tracks as you need to update any property of any object in the entire game, all without creating objects that know too much.

 

 

To apply this to your situation, you would attach a Track to each of the floats on your camera and use the Camera Controller or Network or Player Input to update the values the Track writes out.  A Track is meant just to play an animation over a series of key frames, so by this point you would want to stop calling it a Track and call it "MyBigBadFloatUpdater" or something.

Your Camera has dirty flags to set when its position etc. changes?  No problem.  In my Tracks you pass an optional pointer to a flag and a value to OR into it when writing out a value.  The Track will write directly to your Camera floats and set appropriate dirty flags as needed.

 

I prefer this system as everything gets updated when it should in a controlled environment, and nothing knows too much about the outside world.  A Camera Controller doesn't need to know what a Camera is (even though it would make sense if it did), and a Camera doesn't need to know about anything else.  The Camera Controller just knows about "MyBigBadFloatUpdater" which could be attached to vectors on a camera, or a player, or particle, or even a Hodgman.


L. Spiro

Share this post


Link to post
Share on other sites

If the source hasn't been updated then you just get the same value back anyway.


I legitimately don't know how that is a problem.


If the value is dynamic enough that every request to get an update results in a new value being returned, then you have a performance problem and potentially a sync problem.


That's true regardless of how your plumbing works. Careful order of operations and reliable sequencing are key pillars of game architecture. I don't know why you think this is peculiar to my suggestions.



If only a single source is polling the "vector source" then you have exactly the same relationship as if a camera controller were updating a local once and sending that as the reply, and in this case you could just have the camera controller update "its" vector directly on the camera.


Yes, that is technically true, but you're missing the point of having the directions inverted.



If many sources are trying to poll the "vector source" then you would want to have a mutable copy to send out for each matching request rather than updating it each time.


This doesn't follow. All you do is store the state in a central location (doesn't matter what "object" owns it if you want to be nitpicky) and update it as necessary from the point of authority for that data.

In other words, you publish your data to a storage location, and consumers just query it from there. The frequency of updates is irrelevant and no different a challenge than any other game architecture.


Now how often the camera controller updates needs to be tightly controlled to ensure consistent results.  Each pull should return the same result for the duration of a logical update, or for a single render, etc.


Yes. This does not contradict anything I suggested.

By the time you have gotten this far, the relationship between the camera and the camera controller isn't really, "I, the Camera, call upon the vector source to signal to the Camera Controller to update and give me a value."  The Camera Controller is updating at its fixed rate, buffering the value, and allowing things to read it when they need, which is how most of everything works.  It's exactly the same functionally and performance-wise as the Camera Controller updating its value and writing it to all the Cameras that need it (which again is almost always just 1).  Pull requests will keep getting the same value until the Camera Controller updates, so you are back to the first point in this bullet list.


But what you're describing is exactly what I'm advocating for. I'm confused.

The whole idea of this design is that you can decouple things at a code level without compromising control or performance.


Things have to be tightly controlled for results consistent not just between replays but even inside a frame.  You can't have 2 objects poll the time in a frame and get slightly different results or one will end up falling farther than the other.


This is starting to feel like a straw-man in all honesty. I didn't say you ask for an update, you ask for a value. Updating the values is something I purposefully left vague because it does require the level of control you're describing.

Apparently in my quest to avoid describing every last detail of a relatively simple architectural decision, I have fallen into the trap of leaving too much open for interpretation.

 

If the main concern is really about relationships and what knows about what, I implemented a solution for exactly this inside my animations.
A Track knows only what floats or ints or bools are.  It doesn't know about vectors or any other types of objects.
You pass it a pointer to the float, bool, or int that it is supposed to update, the Track determines values as it interpolates between key values as you update it each frame, and it updates the value to which the pointer points.
To update a vector, you have 3 tracks pointing to 3 floats.  You can have as many tracks as you need to update any property of any object in the entire game, all without creating objects that know too much.


This is shockingly similar to what I'm proposing. The only difference is I would specialize on the case of a 3-vector and you chose not to. Aside from that, we're talking about the exact same concept.


At this point this is echoing the other conversations I've had about the whole thing, which makes me suspect I'm rather shit at explaining myself :-/

Share this post


Link to post
Share on other sites

At this point this is echoing the other conversations I've had about the whole thing, which makes me suspect I'm rather shit at explaining myself :-/

Sorry.
There’s a fine line between requesting something that has been held for you, and holding something to be requested from you.

If you believe that I have described what you wanted to explain with only the modification that you made an object for 3 floats and I suggested handling 3 floats separately, than the we are indeed talking about the same thing.
In my proposal you make a basic object to handle floats and then build on that if you want to handle vectors, but otherwise indifferent.

This is what I have implemented and suggested. If that was not clear, then please explain.


L. Spiro Edited by L. Spiro

Share this post


Link to post
Share on other sites
Since this is pretty much impossible to talk about without code...

https://github.com/apoch/scribblings/tree/master/ValueSourceDemo


This is a very skeletal outline of what I'm talking about. It's simple but demonstrates the shift effectively.

You may be inclined to react with "That's it?!" which is precisely what I'm going for. It isn't a huge change at all. It's a reflection of a basic dependency inversion that everyone ought to be familiar with already.

Note how the classic moving object gets an advance pumped into it while the value-source controlled object has a layer of indirected storage for its position.

Hopefully this code will put everyone on the same page as far as what I'm talking about.

Share this post


Link to post
Share on other sites
Yup, this is basically reactive programming with a uniform abstraction for a data stream so that any stream of the right type can interchange with any other stream.

It's not inherently functional in the sense that you are free to implement a value source in a more traditional imperative advance-loop model.

Share this post


Link to post
Share on other sites

I believe I get what you're going for, if only because I've had similar musings recently  :D

If I may be so bold, perhaps the reason why you're not getting the responses you're hoping for is because you're focused on explaining and demonstrating the mechanics of something, but haven't really described what that "something" actually is or why anyone would want to use it? You've shown what is it to invert the dependencies on objects into dependencies on data, but I think you've stopped short of answering the next logical question, which is "who owns the data source"? And if this example is extended to all game data, then certainly this data has to exist in an organized fashion somewhere. So the next question would be, "what does this system or architecture for storing and manipulating data look like"?

In my mind, what's essentially happening is that you're divorcing the data model from the object model, which allows the two models to exist independently but function cooperatively. Going back to the controller/camera example, conventional wisdom would dictate that each camera object own its own position, and anyone who wants to update or retrieve the position would have to go through the camera. This forces the structure of your data to follow the structure of your objects, which might not always be ideal. But by using a "data source" instead, the camera no longer owns its position and instead just queries some external source, which is also happens to be updated by the camera controller from time to time. In this sense, neither really own the data. You just have objects that manipulate some shared state, to which we've applied the semantic meaning of "camera position".

My own thoughts on the subject lead me to envision some sort of simple hierarchy, where each data "node" can either be an instance of type T, an array of type T, or an association (dictionary) of string to type T. Think JSON, but with the ability for T to also be a pointer to others nodes in the model (for the in-memory representation). The object model can then be created separately, but still reference or "bind" itself to the different parts of the data model. Provide the camera controller with a read/write interface to the position data, and the camera a read-only interface to the position data. If multiple observers want to inspect the position, they too can get read-only interfaces to the position data. If another camera controller comes along and wants a write interface to the position, then the ownership semantics can be handled centrally and uniformly as a feature of the data model. The same goes with push/pull and events/polling. Allow the model to trigger events or callbacks when data changes, and/or for a timestamp or a change flag to be set, and both methods just become opt-in features that allow objects to decide how they want to interact with the model.

Then it becomes clearer how such an architecture could provide you with all the advantages you mentioned in your first post, because everything only has to deal with a single, universal model that's designed specifically around managing data ownership and relationships. This makes replication, serialization, debugging, replays, etc. easier because object relationships and state that's only applicable to the current execution context can be ignored completely.

Hopefully this is somewhat along the lines of what you've been attempting to explain?

Share this post


Link to post
Share on other sites
That's a fair observation.

I partly avoided the subject because it's highly subjective, and partly because it seemed somewhat obvious to me that every game is going to do things a tiny bit differently.

As a starting point, I'd say it's definitely true that you want a centralized point of control for data sources. The hard part is that it is highly situational; what makes sense for one game object may not make sense for another.

But it is absolutely the case that setting a value source (versus just updating one's value) is a tricky question. I'll have a think on ways to express the options and pros/cons.

Share this post


Link to post
Share on other sites
I have something in an engine I'm working on called a DataFlowExpression. It chains together Observable<T> data sources and propagates change events. Observable is just a combination of a value and a 'Changed' event. It's mainly used for the asynchronous 'is this system ready yet? if it's already ready, just do something now' pattern, except without any of the modern frameworks available (I'm stuck with Unity's old Mono libs).

But if you don't want to immediately propagate change, then you've effectively just got an expression tree/first class function, with or without memoization depending on how much optimization you need.

If all you want to solve is the coordination of "who gets control of the camera with what priority, in what circumstances, and in what mixture", you make a top-level class which watches all of the camera data sources, decides how to combine that data, and drives the camera. Edited by Nypyren

Share this post


Link to post
Share on other sites
Sure, the solution for cameras is pretty straightforward.

But how would you, for instance, integrate with a physics engine?

Or control color elements for a shader?

And so on.

Share this post


Link to post
Share on other sites
I'm not sure which parts of a physics engine you'd want to use it for. Can you clarify?

For shader colors, it seems like a simple DataFlowExpression would work, but my gut feeling is that it would end up getting slow if you started using them for an entire system.

The complexities I usually run into with event-driven programming are:
- Sometimes you want bidirectional data flow, and this causes you to pollute everything with 'if (raising) return; raising = true; raise(); raising = false'. Definitely feels like a design issue when that happens.
- Sometimes events are raised too frequently and you just want them in a batch). An extremely common case is adding multiple items to a list, but only wanting to raise an event at the end. You need either an AddSingle or AddMultiple distinction, or just have AddMultiple but be forced to use a collection when passing a single item.
- It can be cumbersome to unsubscribe everything in the event handling structure when you want to shut part of it down.
- You end up with a lot of tiny first-class functions everywhere that are not cache-friendly and potentially using a significant amount of memory if you are really liberal with their use.

I prefer "get it when you need it" programming any time you're doing something every frame. Event-driven is nice when something is happening infrequently enough that the "scattered" nature of the event calls isn't any worse than what you'd normally be doing. Edited by Nypyren

Share this post


Link to post
Share on other sites
I wouldn't event-drive the vast majority of value sources. They're really much better as reactive programming units, or intermediaries between systems.

For example, say I have an NPC game object. The position and velocity of the NPC could be controlled by a physical simulation.

In other words, physics produces position and velocity data for the NPC.

Rendering might consume that data to display the NPC.

There are as many ways to pipe together this producer/consumer relationship as there are game engines.

My proposal is simply to put a buffer zone between the producer and the consumer.


The decoupling and interchangeability of producers and consumers is the key win. The implementation details are mostly irrelevant for the purposes of what I'm advocating for.

But I suppose the hangup at the moment is that, by leaving the implementation details unspecified, I'm not really giving anyone a clear picture of the idea.

Share this post


Link to post
Share on other sites

But I suppose the hangup at the moment is that, by leaving the implementation details unspecified, I'm not really giving anyone a clear picture of the idea.


I think so; without specific details the buffer just seems to boil down to the concept of an 'interface'. If you require a concrete "third party", then it sounds like the Mediator pattern. Edited by Nypyren

Share this post


Link to post
Share on other sites

I don't disagree that there are any number of implementations, but without providing any details it's difficult to analyze further and discuss specific pros and cons. I've often found that the process of translating a concept into an implementation itself can give you a wealth of insight, simply because a tangible representation of an idea can shed light on hidden flaws or benefits that you didn't even realize existed before you had something physical to examine and tinker with. That's partly why I posted basic, high-level implementation details for (my interpretation of) your idea, to at least provide a reference that others could discuss, build upon, tear apart, etc., in concrete form. That, and because your post validated some of my own musings, so thanks!

Share this post


Link to post
Share on other sites

I really like the idea of separating an algorithm from its state. It often makes it easy to have code "self contained". If there's little to no state management, then suddenly the "self-contained" aspect becomes easy to get.

Some contrived examples:

Compressor c = new Compressor( init_params );
c.SetupThingies( );

c.AddInput( data );
c.Compress( );

compressed_data = c.Decompress( );

becomes

Compress( in, out );
Decompress( out, in );

---

Physics p = new Physics( init_params );
b = p.AddBody( );
b.AddCollider( p.MakeShape( ) );

...

p.AddCallback( CollisionHappened );

void CollisionHappened( )
{
    play_sound( );
}

becomes

if ( Hit( a, b ) )
{
    play_sound( );
}

---

The bigger OOP-styled, or heavy hitting "retained mode", libraries often have too many unneeded features. Usually the game wants something specific and has to come up with very odd glue code to wrap things or conform to the library's design. If an API is carefully designed with minimal exposed dependencies and state, and is self-contained, it frees up the game to take on its most natural shape.

Share this post


Link to post
Share on other sites

But I suppose the hangup at the moment is that, by leaving the implementation details unspecified, I'm not really giving anyone a clear picture of the idea.


I think so; without specific details the buffer just seems to boil down to the concept of an 'interface'. If you require a concrete "third party", then it sounds like the Mediator pattern.


I think it's part Mediator and part Strategy, but that's kind of academic.


I'm still contemplating how best to demonstrate this without having to basically write a game :-P

Share this post


Link to post
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now

Sign in to follow this  

  • Advertisement
  • Advertisement
  • Popular Tags

  • Advertisement
  • Popular Now

  • Similar Content

    • By GytisDev
      Hello,
      without going into any details I am looking for any articles or blogs or advice about city building and RTS games in general. I tried to search for these on my own, but would like to see your input also. I want to make a very simple version of a game like Banished or Kingdoms and Castles,  where I would be able to place like two types of buildings, make farms and cut trees for resources while controlling a single worker. I have some problem understanding how these games works in the back-end: how various data can be stored about the map and objects, how grids works, implementing work system (like a little cube (human) walks to a tree and cuts it) and so on. I am also pretty confident in my programming capabilities for such a game. Sorry if I make any mistakes, English is not my native language.
      Thank you in advance.
    • By Ovicior
      Hey,
      So I'm currently working on a rogue-like top-down game that features melee combat. Getting basic weapon stats like power, weight, and range is not a problem. I am, however, having a problem with coming up with a flexible and dynamic system to allow me to quickly create unique effects for the weapons. I want to essentially create a sort of API that is called when appropriate and gives whatever information is necessary (For example, I could opt to use methods called OnPlayerHit() or IfPlayerBleeding() to implement behavior for each weapon). The issue is, I've never actually made a system as flexible as this.
      My current idea is to make a base abstract weapon class, and then have calls to all the methods when appropriate in there (OnPlayerHit() would be called whenever the player's health is subtracted from, for example). This would involve creating a sub-class for every weapon type and overriding each method to make sure the behavior works appropriately. This does not feel very efficient or clean at all. I was thinking of using interfaces to allow for the implementation of whatever "event" is needed (such as having an interface for OnPlayerAttack(), which would force the creation of a method that is called whenever the player attacks something).
       
      Here's a couple unique weapon ideas I have:
      Explosion sword: Create explosion in attack direction.
      Cold sword: Chance to freeze enemies when they are hit.
      Electric sword: On attack, electricity chains damage to nearby enemies.
       
      I'm basically trying to create a sort of API that'll allow me to easily inherit from a base weapon class and add additional behaviors somehow. One thing to know is that I'm on Unity, and swapping the weapon object's weapon component whenever the weapon changes is not at all a good idea. I need some way to contain all this varying data in one Unity component that can contain a Weapon field to hold all this data. Any ideas?
       
      I'm currently considering having a WeaponController class that can contain a Weapon class, which calls all the methods I use to create unique effects in the weapon (Such as OnPlayerAttack()) when appropriate.
    • By Vu Chi Thien
      Hi fellow game devs,
      First, I would like to apologize for the wall of text.
      As you may notice I have been digging in vehicle simulation for some times now through my clutch question posts. And thanks to the generous help of you guys, especially @CombatWombat I have finished my clutch model (Really CombatWombat you deserve much more than a post upvote, I would buy you a drink if I could ha ha). 
      Now the final piece in my vehicle physic model is the differential. For now I have an open-differential model working quite well by just outputting torque 50-50 to left and right wheel. Now I would like to implement a Limited Slip Differential. I have very limited knowledge about LSD, and what I know about LSD is through readings on racer.nl documentation, watching Youtube videos, and playing around with games like Assetto Corsa and Project Cars. So this is what I understand so far:
      - The LSD acts like an open-diff when there is no torque from engine applied to the input shaft of the diff. However, in clutch-type LSD there is still an amount of binding between the left and right wheel due to preload spring.
      - When there is torque to the input shaft (on power and off power in 2 ways LSD), in ramp LSD, the ramp will push the clutch patch together, creating binding force. The amount of binding force depends on the amount of clutch patch and ramp angle, so the diff will not completely locked up and there is still difference in wheel speed between left and right wheel, but when the locking force is enough the diff will lock.
      - There also something I'm not sure is the amount of torque ratio based on road resistance torque (rolling resistance I guess)., but since I cannot extract rolling resistance from the tire model I'm using (Unity wheelCollider), I think I would not use this approach. Instead I'm going to use the speed difference in left and right wheel, similar to torsen diff. Below is my rough model with the clutch type LSD:
      speedDiff = leftWheelSpeed - rightWheelSpeed; //torque to differential input shaft. //first treat the diff as an open diff with equal torque to both wheels inputTorque = gearBoxTorque * 0.5f; //then modify torque to each wheel based on wheel speed difference //the difference in torque depends on speed difference, throttleInput (on/off power) //amount of locking force wanted at different amount of speed difference, //and preload force //torque to left wheel leftWheelTorque = inputTorque - (speedDiff * preLoadForce + lockingForce * throttleInput); //torque to right wheel rightWheelTorque = inputTorque + (speedDiff * preLoadForce + lockingForce * throttleInput); I'm putting throttle input in because from what I've read the amount of locking also depends on the amount of throttle input (harder throttle -> higher  torque input -> stronger locking). The model is nowhere near good, so please jump in and correct me.
      Also I have a few questions:
      - In torsen/geared LSD, is it correct that the diff actually never lock but only split torque based on bias ratio, which also based on speed difference between wheels? And does the bias only happen when the speed difference reaches the ratio (say 2:1 or 3:1) and below that it will act like an open diff, which basically like an open diff with an if statement to switch state?
      - Is it correct that the amount of locking force in clutch LSD depends on amount of input torque? If so, what is the threshold of the input torque to "activate" the diff (start splitting torque)? How can I get the amount of torque bias ratio (in wheelTorque = inputTorque * biasRatio) based on the speed difference or rolling resistance at wheel?
      - Is the speed at the input shaft of the diff always equals to the average speed of 2 wheels ie (left + right) / 2?
      Please help me out with this. I haven't found any topic about this yet on gamedev, and this is my final piece of the puzzle. Thank you guys very very much.
    • By Estra
      Memory Trees is a PC game and Life+Farming simulation game. Harvest Moon and Rune Factory , the game will be quite big. I believe that this will take a long time to finish
      Looking for
      Programmer
      1 experience using Unity/C++
      2 have a portfolio of Programmer
      3 like RPG game ( Rune rune factory / zelda series / FF series )
      4 Have responsibility + Time Management
      and friendly easy working with others Programmer willing to use Skype for communication with team please E-mail me if you're interested
      Split %: Revenue share. We can discuss. Fully Funded servers and contents
      and friendly easy working with others willing to use Skype for communication with team please E-mail me if you're interested
      we can talk more detail in Estherfanworld@gmail.com Don't comment here
      Thank you so much for reading
      More about our game
      Memory Trees : forget me not

      Thank you so much for reading
      Ps.Please make sure that you have unity skill and Have responsibility + Time Management,
      because If not it will waste time not one but both of us
       

  • Advertisement