Search Unity

[Released] Kinematic Character Controller

Discussion in 'Assets and Asset Store' started by PhilSA, Sep 29, 2017.

  1. PhilSA

    PhilSA

    Joined:
    Jul 11, 2013
    Posts:
    1,926
    I managed to repro this in my scene by using walls made out of cubes:
    https://i.gyazo.com/ac28b7db91b57d46f54bf90d0183b12e.mp4

    I'll take a look at this during the weekend. It's definitely a problem with my step handling
     
    Shinyclef likes this.
  2. Shinyclef

    Shinyclef

    Joined:
    Nov 20, 2013
    Posts:
    505
    Successful bug report!
    Still congrats on the step handling regardless. My character can how fit through gaps 1 unit wide and step up 0.5 steps.
     
  3. maxaud

    maxaud

    Joined:
    Feb 12, 2016
    Posts:
    177
    Thanks, I'll try it out!
     
  4. Shinyclef

    Shinyclef

    Joined:
    Nov 20, 2013
    Posts:
    505
    A little more investigation to help your efforts.
    Here is a character jumping along a wall of stacked box colliders.
    Each block has it's own box collider.
     

    Attached Files:

    PhilSA likes this.
  5. maxaud

    maxaud

    Joined:
    Feb 12, 2016
    Posts:
    177
    I had to convert the velocity to local space and divide by the max stable speed to get a proper result for my blendtree but this works pretty well for character velocity based animation.
    Code (CSharp):
    1.  
    2. Vector3 planarVelocity = Vector3.ProjectOnPlane(Motor.BaseVelocity, Motor.CharacterUp ) / MaxStableMoveSpeed;
    3. Vector3 localdirection = Motor.Transform.InverseTransformDirection( planarVelocity );
    4.  animator.SetFloat("Velocity Z", localdirection.z);
    5.  animator.SetFloat("Velocity X", localdirection.x);
    6.  
     
  6. VagabondOfHell

    VagabondOfHell

    Joined:
    Sep 17, 2013
    Posts:
    15
    Thanks for that PhilSA. I was missing a Cross Product by the time I saw the post yesterday!

    In orbit camera I added a public bool called UseGlobalUp, and if true I replace the character ups with the vector3.up.

    I had to place your script into your swim sample to see what you meant. I never had that problem because I added a smooth transition for Gravity Orientation when I went through the gravity sample, because I wanted to control the direction of the rotation along the characters' forward vector (since it's first person, prevented looking like you were doing backflips when gravity changed). This interpolation seems to have removed that issue to an extent.

    I did end up adding some land checking as well before switching to default state to make it a little smoother. Using your recent update with SetPosition and SetRotation made this easier. The only issues with my changes I've found so far are:

    1) In 3rd person the character will often come out of the water rotated 180 on Y, so you see the character spin around. Not an issue in my case, but for others it could be.

    2) Moving along the characters' right vector to a slope too steep to be considered ground can lift you out of the water in the swim state, where you can swim around above the surface of the water. I'll be fixing this one today (probably just checks with the water normal and if too high, adjust)

    I've added the files I've modified in case it helps you or someone else with implementing such functionality. My primary focus is that it works in 1st person, so I can't guarantee it will work for everyone. In addition, I changed the rotation in swim to use the camera's forward direction instead of the characters movement velocity. So hopefully that's the only change for people who want to do this in 3rd person from my code.

    Thanks again PhilSA! If you end up browsing the files, feel free to suggest improvements if you think I'm using your code in an inefficient way.
     

    Attached Files:

    PhilSA likes this.
  7. PhilSA

    PhilSA

    Joined:
    Jul 11, 2013
    Posts:
    1,926
    it's weird; I'm not able to repro this particular case. Could you give me:
    • screenshot of your physics settings (Edit > Project Settings > Physics)
    • screenshot of your KinematicCharacterMotor settings
    • Unity version used? I've sometimes observed different behaviour in 2017.1 compared to the later ones
     
  8. Shinyclef

    Shinyclef

    Joined:
    Nov 20, 2013
    Posts:
    505
    Ah I'm off to work now. Will be about 11 or so hours before I'm home. I'll tell you what i remember for now.

    Physics are basically default settings.
    KCC has 1.8 height, 0.45 width, advanced step logic with 0.501 step height. No wall or double jump.
    Using unity 2017.3, i think p1.

    I'll confirm all this when home.
     
  9. PhilSA

    PhilSA

    Joined:
    Jul 11, 2013
    Posts:
    1,926
    @Shinyclef I might have a fix for you to try out. Since I couldn't yet repro the case of getting stuck on a high wall, I'm not totally sure it fixes everything. But it'd be worth a try

    Is there a way I can send you the fixed KinematicCharacterMotor privately? Your PMs are deactivated I think
     
  10. Shinyclef

    Shinyclef

    Joined:
    Nov 20, 2013
    Posts:
    505
    I think I've fixed my PM issue now :).

    BTW, I noticed "// Complex stepping not supported on dynamic rigidbodies" in your Motor. Will it just not work? If possible I'd like to enable it... but I get the feeling removing conditionals randomly will get me into trouble... haha
     
  11. PhilSA

    PhilSA

    Joined:
    Jul 11, 2013
    Posts:
    1,926
    It'll often work fine, but once in a while it'll "bump" into the rigidbody: https://i.gyazo.com/f1168fa67be3bbd42d102f8c67297898.mp4

    I still have to figure that problem out at some point
     
  12. Shinyclef

    Shinyclef

    Joined:
    Nov 20, 2013
    Posts:
    505
    Right I see. Probably no problem I think I misunderstood. I don't need dynamic rigidbodies, just kinematic ones that will be moving like platforms :). Cheers.
     
  13. PartyBoat

    PartyBoat

    Joined:
    Oct 21, 2012
    Posts:
    97
    Hey PhilSA,

    The latest update looks great, lots of nice improvements since I first bought KKC! I noticed that you've been talking about a networking example for this asset and I was wondering how fully featured we should expect this example to be? Are we talking full on client-side prediction and server reconciliation (ala Gabriel Gambetta's - Fast Paced Multiplayer)? This is something I'm working on soon so I'm wondering if I should forge ahead on my own solution or wait for yours.

    Thanks!
     
  14. PhilSA

    PhilSA

    Joined:
    Jul 11, 2013
    Posts:
    1,926
    It'll have full client side prediction and lag compensation as the linked article describes.

    However I can't make any promises about the reusability of the whole network framework I will make for the example. It'll be very bare-bones and made with LLAPI. No handy [RPC] or [syncvar] attributes or stuff like that. All serialization will be written by hand, most likely
     
  15. PartyBoat

    PartyBoat

    Joined:
    Oct 21, 2012
    Posts:
    97
    Wowza, sounds great! I look forward to it!
     
  16. JustinLarrabee

    JustinLarrabee

    Joined:
    Feb 3, 2013
    Posts:
    13
    That's a good thing IMHO. The HLAPI enforces a lot of patterns that I'd personally rather not be victim to. Any chance your usage of the LLAPI could be abstracted in such a way that the backend could be swapped to, say, use the Steamworks socket API (see this wrapper)? Unity has claimed they will add support for switching the socket layer for the LLAPI to allow such a change, but they have not given any kind of timeline.
     
  17. snacktime

    snacktime

    Joined:
    Apr 15, 2013
    Posts:
    3,356
    I hit a bug in UnregisterCharacterMotor on this line:
    Code (csharp):
    1.  
    2. CharacterMotors[motor.IndexInCharacterSystem] = CharacterMotors[_characterMotorsCount - 1];
    3.  
    I'm calling unregister right after instantiating the prefab in some cases, so it was probably not registered before I called unregister. So it should be checking to see if it's registered.

    Also curious why you are using arrays there and taking the hit of creating a new one to resize? There is no efficiency there to using an array over List<T>, and I care a lot more about creating new arrays all the time then a bit of extra memory for items removed.
     
  18. PhilSA

    PhilSA

    Joined:
    Jul 11, 2013
    Posts:
    1,926
    As of right now, pretty much all of the LLAPI/Networking code is wrapped into one single class. Should be pretty easy to convert to other APIs. I'll keep that in mind as I progress
     
    Last edited: Feb 7, 2018
  19. PhilSA

    PhilSA

    Joined:
    Jul 11, 2013
    Posts:
    1,926
    I see. Right now you can avoid the unregister problem by disabling the KinematicCharacterMotor component after instantiating (myMotor.enabled = false). It'll prevent it from registrating itself in the first place. I'll look into doing some checks to prevent the error in the future

    As for the arrays thing, adding/removing from lists creates GC allocations, which are performance killers (they are often responsible for creating stutters in framerate and making things feel generally un-fluid). So instead, I pre-initialize arrays with a given size, so they will never need to create allocations. There is a mecanism for the array to grow in size if needed, but usually you'll want to avoid that as much as possible, and just give the array a sensible starting size that's adapted to your game

    In general, I try to code all my games with zero GC allocs. At 144 fps the GC.Collect stutters are very obvious
     
    Last edited: Feb 7, 2018
  20. snacktime

    snacktime

    Joined:
    Apr 15, 2013
    Posts:
    3,356
    No lists do not create garbage on remove. And you are creating far more by creating an entire new array instead of just letting List expand as needed.

    You are basically re allocating the array on EVERY add and remove. Please stop.
     
  21. snacktime

    snacktime

    Joined:
    Apr 15, 2013
    Posts:
    3,356
    Ok well not every add unless it goes over the limit.
     
  22. Shinyclef

    Shinyclef

    Joined:
    Nov 20, 2013
    Posts:
    505
    Lists use arrays internally, starting with an initial length. When you try to Add() beyond the capacity of the array, it is replaced with a larger array (2x capacity maybe?), the elements are copied to new array, and the original array becomes obsolete and will be GCed.

    You can set an initial capacity with list pretty easily with new List<T>(int capacity) constructor if you know the size you will need in advance, to prevent memory allocations. If you don't exceed the current capacity (List.Capacity), then adding and removing does not create garbage.
     
  23. snacktime

    snacktime

    Joined:
    Apr 15, 2013
    Posts:
    3,356
    If you look at what List<T> does on Remove it's actually kind of cool, they use Array.Copy to self to basically shift everything and avoid allocating a new array.

    There aren't a lot of reasons to use arrays over List<T>. If you are doing things where you need to be copying chunks of it around or similar stuff, and really high throughput where the extra sanity checks of List<T> could have an impact. But hardly anyone in Unity is doing that kind of thing.
     
  24. PhilSA

    PhilSA

    Joined:
    Jul 11, 2013
    Posts:
    1,926
    Like I said, the goal is zero GC, so one Add() would be too much. The resizing mecanism when it goes over limit is just there for safety. Normally you'll want to give the array an initial size that can contain all the characters you'll need

    Lists are also a bit costlier to iterate on, and a .Count is costlier than a .Length
     
  25. PhilSA

    PhilSA

    Joined:
    Jul 11, 2013
    Posts:
    1,926
    You're right, it's just that the lists in KinematicCharacterSystem are getting used so often and on potentially so many characters that I wanted to take some extra time to make sure they are done in the fastest way possible

    The importance of this optimization is certainly debatable, though. Maybe that's just me getting too impatient for the ECS
     
    Last edited: Feb 7, 2018
  26. snacktime

    snacktime

    Joined:
    Apr 15, 2013
    Posts:
    3,356
    Your game would stop running by the time the performance of list is even measurable, because physics calls dwarf it by so much. And ECS and the job system won't change that, that's how small the difference is.

    If you are going to re implement list, at least copy their approach to resizing so you don't create tons of garbage when unregistering.
     
  27. PhilSA

    PhilSA

    Joined:
    Jul 11, 2013
    Posts:
    1,926
    I may just go with a List + option to set capacity. It's true that the gains of the current approach are minimal
     
  28. PhilSA

    PhilSA

    Joined:
    Jul 11, 2013
    Posts:
    1,926
    @snacktime
    Actually, I just remembered another reason why I chose the arrays method.

    Each character knows its index in the array, making removing characters cost basically nothing. When we remove a character, we simply replace its slot in the array with the last character in the array, and update that character's index. This means the list remains packed at all times, so we don't have to iterate over the whole array. A List.Remove() can become very heavy in comparison if there are 100-300 characters. I cannot use the index strategy with lists, because everyone's indexes may change every time something is removed.

    I gotta think about this some more. In the end I don't think anything beats the arrays method with a pre-determined size for performance. (resizing is a safety precaution, but normally you would plan initial capacities so that it never has resize). But the downside is that it can be a worse solution than Lists for people who aren't aware of all this

    I will definitely add a "KinematicCharacterSystem.SetCharactersCapacity()" though.
     
    Last edited: Feb 8, 2018
  29. snacktime

    snacktime

    Joined:
    Apr 15, 2013
    Posts:
    3,356
    List<T> just expands it doesn't shrink the backing array. The current KC implementation shrinks the array. So assuming you set an initial capacity and never go over it. If you are using unregister/register on the same controllers several times the KC implementation causes an allocation for every remove then add. A List<T> implementation would result in zero allocations.

    If you go over capacity the KC implementation is worse here also. Because it's very unlikely that anyone goes over by just one. Which is why collections expand by some larger number.

    Just fixing the resize on remove isn't really viable either, I posted that too quickly. Because then you would have to deal with what List<T> does, which is account for values removed even though the array is some size larger.

    Most users most likely won't hit the above scenarios. I did because 2017 physics performance took such a hit that I'm completely physics bound. I only enable controllers when actually moving and I enable/disable them in large chunks at certain times also. And the current implementation creates a lot of garbage in this scenario.

    Ya list Remove is an issue, but note that it's still far more efficient then allocating a new array like you are. Removing a controller is anything but free in the current implementation.

    To fix that you can just use a Dictionary with an int key, assign a unique id per controller. foreach on Dictionary does not produce any garbage at least not in 2017. And pretty sure this doesn't in older versions as well:

    Code (csharp):
    1.  
    2. var enumerator = TestDict.GetEnumerator();
    3.                 try
    4.                 {
    5.                     while (enumerator.MoveNext())
    6.                     {
    7.                         var entity = enumerator.Current.Value;
    8.                     }
    9.                 }
    10.                 finally
    11.                 {
    12.                     enumerator.Dispose();
    13.                 }
    14.  
     
  30. snacktime

    snacktime

    Joined:
    Apr 15, 2013
    Posts:
    3,356
    Also, anything approaching 100 characters gets expensive, even list Remove is white noise unless you are doing it a LOT like I am. I replaced the current implementation with List<T>, so now I'm not seeing all the garbage creation, and the Remove barely registers, it's dwarfed by physics. O(N) operations on small lists are pretty cheap.

    Basically you are giving up correct error checking to be able to use the controller index like you are. The add/remove logic is prone to errors where that index would be wrong. Fortunately it fails by throwing an exception right now, but another implementation it could just end up pointing to the wrong data.

    Actually HashSet would probably be the best option here. Haven't tested that myself for it if creates garbage on iteration.
     
  31. snacktime

    snacktime

    Joined:
    Apr 15, 2013
    Posts:
    3,356
    Ya HashSet looks like the best option. I populated one with 1000 class instances and ran this method in Update. Both the GC stats in the code and the profiler showed no allocations.

    Code (csharp):
    1.  
    2. private void HashSetTest()
    3.         {
    4.             var before = System.GC.GetTotalMemory(false);
    5.             for (int i = 0; i < 1000; i++)
    6.             {
    7.                 foreach(var entry in TestHashSet)
    8.                 {
    9.  
    10.                 }
    11.             }
    12.             var after = System.GC.GetTotalMemory(false);
    13.             var allocated = after - before;
    14.             if (allocated != 0)
    15.             {
    16.                 Debug.LogFormat("HashSet GC {0}", allocated);
    17.             }
    18.         }
    19.  
     
  32. PhilSA

    PhilSA

    Joined:
    Jul 11, 2013
    Posts:
    1,926
    I think there is a misunderstanding here. Everything is allocated only once at the start, and registers/unregisters do not grow or shrink anything unless we go over the limit, or back under the initial capacity (although I think I should remove the code that shrinks it. Not very useful I think, and also kinda random)

    As a test, placing this code in one of KinematicCharacterMotor's updates causes no GC Allocs at all:
    Code (CSharp):
    1.  
    2.             for(int i = 0; i < 100; i++)
    3.             {
    4.                 KinematicCharacterSystem.UnregisterCharacterMotor(this);
    5.                 KinematicCharacterSystem.RegisterCharacterMotor(this);
    6.             }

    I do think the whole discussion boils down to this. My preference in that case would be to favor the most optimized approach, which is pre-initialized arrays and not going over capacity. It's such a small amount of code to maintain that I don't mind spending time on it to make sure it doesn't have errors

    ah... didn't think about HastSets. I agree it seems to be the ideal solution. I'll just read a bit about iteration performance on them, just to make sure
     
  33. snacktime

    snacktime

    Joined:
    Apr 15, 2013
    Posts:
    3,356
    Ah ok it was happening to me because my total number was over original capacity by a fair amount. Didn't notice the extra condition on resize only if over capacity. So basically was hitting half of my controllers.
     
  34. PhilSA

    PhilSA

    Joined:
    Jul 11, 2013
    Posts:
    1,926
    Just tried with HashSet and iteration performance is the same. However... I don't know of any way to give it an initial capacity. This might be a problem

    EDIT: ...or not. The unregister/register test shows no allocs. Not sure I understand what's going on. Is HashSet magical enough to not allocate anything because it knows the element is unique, and therefore it's already allocated somewhere? If so, color me surprised
     
    Last edited: Feb 8, 2018
  35. snacktime

    snacktime

    Joined:
    Apr 15, 2013
    Posts:
    3,356
    Ya not really sure what the behavior of HashSet is when you are constantly adding/removing. I know it does trim excess at times.

    If it trims all the time that might be bad. Allocations that are one off events I don't think really matter. Like the automatic expansion I'd happy live with to gain more idiomatic code because so what you eat a couple of those on startup in the normal case. But if HashSet is aggressive with trimming that could be an issue.
     
  36. PhilSA

    PhilSA

    Joined:
    Jul 11, 2013
    Posts:
    1,926
    Allright screw it, I'll just go with Lists and Capacities! All this talk has convinced me that I prefer peace of mind in the end
     
  37. MikeFish

    MikeFish

    Joined:
    Oct 24, 2015
    Posts:
    12
    Hey there,
    Is there a way to make the character act like it is standing on a moving platform (using the PhysicsMover component), without it being actually grounded on it? Setting KinematicCharacterMotor.HandlePhysics(false, true); is not an option here, because i actually need the physics calculations for being pushed off the position by obstacles.

    I'm still working on my ledge hanging, and i got it working pretty nicely, but then I wanted it to work on moving objects and ran into some issues.

    I tried adding the velocity of the moving wall to the character, but it looked like it was always one frame behind.
     
    Last edited: Feb 8, 2018
  38. PhilSA

    PhilSA

    Joined:
    Jul 11, 2013
    Posts:
    1,926
    It'll require making a tiny modification to the KinematicCharacterMotor:
    1. In KinematicCharacterMotor, add a public field representing the PhysicsMover you want to "attach" to:
    2. In the "Interactive Rigidbody Handling" region at the end of UpdatePhase1(), there is a chunk of code that starts with a "// Detect interactive rigidbodies from grounding" comment. Wrap that chunk of code in the if/else statement as shown below. The part highlighted in blue is the original code. You must add the if/else part where if we have an AttachedPhysicsMoverOverride, we set the StableInteractiveRigidbody to that
    And I think that's all! I didn't test this extensively, but it seems to be working. Let me know how it goes.

    I will most likely add that "officially" to the KinematicCharacterMotor soon.
     
    MikeFish likes this.
  39. MikeFish

    MikeFish

    Joined:
    Oct 24, 2015
    Posts:
    12
    Nice, thanks a lot @PhilSA , didnt have time to test it, but my quick workaround so far was making the setter for StableInteractiveRigidbody public and just assign the rb of my mover every frame. As that "worked", I have no doubt this will work as well and better, plus being much much nicer, and solve a couple of problems my quick hack inflicted.

    Great asset mate, awesome support!
     
  40. TheCreativeRoach

    TheCreativeRoach

    Joined:
    Dec 29, 2013
    Posts:
    9
    Good afternoon! First things first, this asset is great; I'm having a great time porting my player controller- wich used the default unity character controller - and the results have been impressive so far.

    However, I detected some inconsistencies when the character runs from the edge of a ramp/stairs, so I started testing it with a new default project to see if I was doing something wrong.

    I have tested this in the scene that you provide, using mostly the default settings for the Example Character Controller and Kinematic Character Motor - also with the default physics, timescale, quality settings...-.

    The setup is simple: I disabled the air acceleration so the input doesn't mess with the results. Then I tried just running at max speed up to the end of the ramp (just changing the "Max stable movement speed" and "Air acceleration speed" to 3 different values). I marked each landing position with a cube. Then I also tried locking the camera and adding a planar constraint just in case it mattered (it did not).

    As you can see in the attached screenshot, at high speeds the landing position varies quite a lot (as does the speed of the character as he "jumps" from the edge).
    I also tested with the 3 different "Step and Ledge Handing methods", and while the results were different, they were also inconsistent in a similar manner.

    I'm not an experienced programmer, so I wouldn't know if this issue comes from the example controller that you provide, the way I'm changing the character speed (maybe I'm not setting the properties correctly) or the way the core handles ledges.

    So I would love to ask for some assistance here, it's quite important in a precission platformer that the results are as consistent as possible.

    Have a nice day! Cheers!. - PS. sorry if my english isn't very good, it ain't my native language .-
     

    Attached Files:

  41. PhilSA

    PhilSA

    Joined:
    Jul 11, 2013
    Posts:
    1,926
    I see what you mean. I did the launching-off-slope test on my end and arrived to similar conclusions. In this pic I've drawn red lines at the landing positions of each jump:


    I'll look into making this more deterministic for the next update. I think I have an idea of what's causing this variation
     
  42. PhilSA

    PhilSA

    Joined:
    Jul 11, 2013
    Posts:
    1,926
    Last edited: Feb 11, 2018
    Petethegoat and Guacamolay like this.
  43. Shinyclef

    Shinyclef

    Joined:
    Nov 20, 2013
    Posts:
    505
  44. TheCreativeRoach

    TheCreativeRoach

    Joined:
    Dec 29, 2013
    Posts:
    9
    Woah, that was fast! It looks like it's working perfectly, now I just have to wait the new release =). Thank you for your prompt response.
     
  45. PhilSA

    PhilSA

    Joined:
    Jul 11, 2013
    Posts:
    1,926
    The landing positions when jumping off of a slope could vary quite a lot, even when the character jumped at the exact same velocity, as shown in this post

    Now I've made it so that the jumping trajectory is always exactly the same
     
  46. zibas

    zibas

    Joined:
    Sep 15, 2010
    Posts:
    31
    Hey, enjoying the system. Unless I'm missing something, It looks like you're assuming centered colliders when calculating rigidbody angular rotation transfer to the player. This is causing the character to slip on colliders with non-zero center points.

    Here's a terrible quick hack that fixes it. I'm sure you can do better than me.

    Goes inside: KinematicCharacterMotor.cs: GetVelocityFromRigidbodyMovement

    Code (CSharp):
    1.    
    2. if (interactiveRigidbody.GetComponent<BoxCollider>() != null) {
    3.       centerOfRotation = interactiveRigidbody.transform.TransformPoint(
    4.                                    interactiveRigidbody.GetComponent<BoxCollider>().center );
    5. }
    Cheers!
     
  47. Guacamolay

    Guacamolay

    Joined:
    Jun 24, 2013
    Posts:
    63
    Hey, quick question. I'd like to be able to constrain the movement of the character to a plane which has a normal, but also a "position" in world space. For example, the normal of the plane would be Vector3(1, 0 ,0) and the distance from the origin would be 20. So the character would be moved and constrained to the plane wherever it is on the map.

    This plane could change at any time and I'd like the character to adjust their position to stay on the plane (e.g. Change from Vector3(1, 0 ,0) to Vector3(0.7, 0.7, 0) or updating the plane normal to create a smooth round curve that the character be constrained to)

    Right now, I get my plane, project the current character velocity onto the plane, and use Motor.MoveCharacter(targetPos) to set the character to the plane position in world space. This is all done in AfterCharacterUpdate.

    Is this a correct solution? I find it works well, but I may be missing something, or there may be a better way to do it
     
  48. PhilSA

    PhilSA

    Joined:
    Jul 11, 2013
    Posts:
    1,926
    You're absolutely right! Fixing this now

    Thanks
     
  49. PhilSA

    PhilSA

    Joined:
    Jul 11, 2013
    Posts:
    1,926
    You should go with Motor.SetPosition(targetPos) instead of MoveCharacter

    MoveCharacter is is constrained by collisions and planar constraints, but SetPosition is a direct 'teleportation' that isn't constrained by anything
     
    Shinyclef likes this.
  50. PhilSA

    PhilSA

    Joined:
    Jul 11, 2013
    Posts:
    1,926
    Wait, I completely misunderstood your question in my last reply. Your solution will actually work just fine!
     
    Guacamolay likes this.