Unity GameCenter Support

I’ve spend the last couple of days implementing GameCenter support in¬†a Unity game and found more than a few pitfalls all of which goes back to a seriously broken Unity API.

So, if you are¬†planning on using the Social API in Unity3D, here’s a couple of landmines you may not want to step on (I’m sure there are others, but these are the ones that I wasted time on) – oh, and just for the record

1) I love Unity – it’s an awesome platform. It’s just their GameCenter implementation of the Social API that could do with a bit of love ūüôā

2) These issues were found in Unity 4.5 (but Google suggests they have been there forever, so I would not count on them being fixed anytime soon)

Loading scores

The first thing that bit me is that ILeaderBoard.LoadScores only supports a single request at a time. If you try to call it multiple times, only the last invocation will receive a callback (several times, whatever good that will do you). I implemented the following helper to work around the issue:

  struct LoadScoreRequest
  {
    public string            _id;
    public string[]          _userIds;
    public BoardLoadedEvent  _callback;
  }

  public delegate void BoardLoadedEvent(ILeaderboard board, HighScore[] highScore);
  
  private bool _friendsLoaded;
  private bool _loadScoreRequestPending;
  private List<LoadScoreRequest> _loadScoreRequests = new List<LoadScoreRequest>();

  public void CreateAndLoadLeaderBoard(string id, string[]userids, BoardLoadedEvent ondone)
  {
    lock(_loadScoreRequests)
    {
      _loadScoreRequests.Add( new LoadScoreRequest() { _id=id, _userIds = userids, _callback = ondone }  );
      SendNextScoreRequest();
    }
  }

  private void SendNextScoreRequest()
  {
    LoadScoreRequest req;
    lock(_loadScoreRequests)
    {
      if(!_friendsLoaded)
        return;

      if(_loadScoreRequestPending)
        return;

      if(_loadScoreRequests.Count==0)
        return;

      _loadScoreRequestPending = true;
      req = _loadScoreRequests[0];
      _loadScoreRequests.RemoveAt(0);
    }

    if(req._userIds==null)
    {
      req._userIds = new string[Social.localUser.friends.Length];
      int i=0;
      foreach(IUserProfile friend in Social.localUser.friends)
      {
        req._userIds[i++] = friend.id;
      }
    }
    
    ILeaderboard board = Social.CreateLeaderboard();
    board.id = req._id;
    board.SetUserFilter(req._userIds);

    board.LoadScores( (bool scoresloaded) =>
    {
      req._callback(board);
      lock(_loadScoreRequests)
      {
        _loadScoreRequestPending = false;
        SendNextScoreRequest();
      }
    });
  }

Basically, it queues up all requests until the GameCenter authentication has been completed and the friends list has been loaded. It then starts loading the leaderboards one at a time, making sure each board gets returned to the proper callback.

The¬†authentication code is not shown above, but it’s straight forward and simply sets the _friendsLoaded member to “true” after loading the friends list and then calls SendNextScoreRequest in case any requests got queued up while authenticating.

Saving scores

IScore.ReportScore does not work.

At least, I could not get it to work. Use Social.ReportScore() instead as it seems to work as advertised, and has the added advantage that you don’t need to carry around the ILeaderboard reference. Why the broken method is even in the public API I can only guess at.

Error handling

Basically, there isn’t any.

At least, you can forget about checking the boolean “success” parameter provided in the various callbacks. I’ve never seen this have a value other than “true” despite a myriad of issues, none of which I’d personally characterise as a “success”.

Instead, check that the data you expected is available Рfor example, that the user- or highscore-lists are not empty.

UPDATE & WARNING:

Be really careful with Social.LoadAchievementDescriptions – it will load the associated images for all achievements and keep them in memory. They will not be released again.

In my case it amounted to 12 images of¬†1024×1024 pixels which, with some overhead of unknown origin, amounted to 96MB each time the game refreshed the achievements list.

There’s a similar problem with loading user profiles – their profile images are also kept in memory and are not released when you let go of the profile itself.

Throw to hit

Most people probably don’t consider the intricacies of computer-game enemies shooting at them. In many cases rightfully so, since it’s mostly a very simple feature. The game code to send a¬†fast moving projectile towards you rarely involves more than a subtraction¬†of two points to get a direction, and multiplying with¬†whatever speed bullets¬†happen to travel at in the given game. If it’s fast enough, it’ll hit you. Other times, bullets are instant hit and the game¬†simply checks for intersection with a line segment – no chance of escaping that one either.

In¬†the¬†game I’m currently working on, however, most of the enemies¬†don’t shoot bullets; they throw water balloons. Just as in real life, timing and aiming a throw is quite a bit more involved¬†than firing a bullet, and¬†while most of us learn this as kids by trial and error, a game usually solves it with a bit of math.

It really isn’t much more complicated than the two scenarios I just described, but it does involve¬†matching a polynomial¬†with a linear¬†interpolation, so if both of those sound alien to you, and you happen to be faced with this problem, read on :).

The Stage

Before we look at the math, here’s a quick overview of the problem we’re faced with:

Throwing B at P

Throwing B at P

Let’s say we’re the enemy and we’re at location B0, throwing at the player who is currently at P0. What we want is to find the velocity F that our projectile B must travel with so that it will impact with P when the gravity (g) pulls the projectile to the ground. N is the current velocity of the player.

The path of the projectile is given by the second order polynomial

B = B0 + F*t + G*t²

The path of the player is given by the linear equation

P = P0 + N*t

Assuming, of course, that he does not change direction or speed – which is kind of the whole point of the game ūüôā

Before we try to solve this, note that the G vector is special in that it only affects one axis. This simplifies matters a great deal since our second-order polynomial then only exist for this axis, and hence, we can separate the motion in the X/Z plane from that of the Y axis. Or more specifically, we can ignore X/Z and pick a velocity in Y that creates a nice curve and gives us a time of impact, and then adjust our projectile speed in X/Z to match the players location at the calculated time.

So let’s look at the equation for Y:

B0.y + F.y*t + G.y*t² = P0.y + N.y*t

For some rather game-specific reasons, I don’t care if the player moves in Y, so my N.y is always¬†0. It should be straight forward to change the code to take this into account, you just need to carry the N.y term along.¬†Anyway, in my case,¬†I can simplify to:

G.y*t² + F.y*t + (B0.y -P0.y)  = 0

Which is the 2nd order polynomial we need to solve to determine our time of impact, t. If this doesn’t ring any bells, just google it, there’s a text-book solution for this, and chances are you’ve solved a million of these in school and¬†just forgotten about it ;).¬†Below I’ll just go through the code.

Code

This is what a prettified version of my implementation looks like:

Vector3 P0 = player.transform.position;
Vector3 B0 = enemy.transform.position + initialOffset;
Vector3 F  = (P0-B0);
float l = F.magnitude;
          
if(l>1)
{
  F.y = l*.5f;
            
  Vector3 G = new Vector3(0,-9.81f/2,0);
  Vector3 N = player.transform.forward * player.runningSpeed;
            
  float t = SolvePoly(G.y, F.y, B0.y-P0.y);
  if(!float.isNaN(t))
  {
    F.z = (P0.z-B0.z) / t + N.z;   
    F.x = (P0.x-B0.x) / t + N.x;   
    WaterBalloon b = (WaterBalloon)Instantiate(_waterBalloon);   
    b.transform.position = B0;   
    b.rigidbody.velocity = F;
  }          
}

In short, this starts by picking a Y velocity that is proportional to the distance to the target. This creates a higher arc for targets that are further away. With F.y set, I can solve the second order polynomial for Y and derive the time of impact “t”, which I then use¬†to calculate the remaining two components of F.

F is now my initial velocity which I can assign directly to my (non kinematic) rigidbody in Unity. If, for some reason, you are using a kinematic projectile, you’ll need to move it yourself in the FixedUpdate method. Something along the lines of

transform.position += _velocity * Time.fixedDeltaTime;
_velocity += _gravity * Time.fixedDeltaTime;

should do the trick.

Comments

There’s a couple of details in this code that are maybe¬†not immediately obvious:

Line 2: I add an initial offset to the enemy location to get the projectile start location. This is simply because the enemy is supposed to throw the balloon, not kick it – I.e. I want it leaving his hands, not his feet ūüôā

Line 8: I set the Y velocity to half the distance between the enemy and the player. This is a more or less arbitrary choice – the smaller the number, the flatter the curve. Note that this value alone determines time-to-impact, so if you keep it constant, short throws will take just as long to hit as long throws.

Line 10: If you look closer at the definition of G you’ll notice that gravity is defined to be only half of earths normal gravity of 9.81. The reason for this is that the polynomial is describing the curve of the projectile which is the integral of its velocity, not the velocity itself. If you differentiate to get the velocity of the curve:

p = p0 + v*t + k*t² =>

p’ = v + 2*k*dt

…and¬†insert g/2 for k, you will see that the change in velocity caused by gravity correctly becomes g*dt.

Line 11: I calculate the players velocity using his forward direction and a speed – if the target is a non-kinematic rigidbody you could probably use the velocity directly, but mine isn’t.

Line 13: As you probably know, a second order polynomial has two solutions РI, however, only calculate one of them. If you plot the curve, the reason will be obvious: The polynomial will cross zero twice between thrower and target. Once going up, and once coming down Рwe want the one coming down which is obviously the larger t and because of the nature of our particular set of coefficients, this is always the solution described by s1 below:

private float SolvePoly(float a,float b,float c)
{
   float sqrtd = Mathf.Sqrt(b*b - 4*a*c);
   float s1 = (-b - sqrtd)/(2*a);
   return s1;
}

Line 14:¬†It is¬†worth noting that this *can* fail. If the chosen Y velocity is not sufficiently large to bring the projectile to the same height as the target location, the second order polynomial will have no solutions, and sqrt will return NaN, hence the check for NaN. You¬†*could* check for this in SolvePoly before doing the sqrt, but in my case¬†it’s such a rare occasion that¬†I’m not too concerned with the performance penalty.¬†I’m happy as long as it does not create any visual artifacts.

Lines 16-17: These two equations are easily derived from the original equations for projectile position B and players position P, like this

B0 + F*t + G*t² = P0 + N*t =>

And then solve for F, noting that G is zero in the cases of X and Z which are the ones we are currently interested in.

 F*t = P0 РB0 + N*t =>

F  = (P0 РB0) / t + N

Enclosure Progress

It’s my own damn fault for not paying attention to the little details, I know. But it was still annoying to find that even though Unity4 supports terrain objects on mobile devices, it doesn’t really “support terrain objects on mobile devices”. As soon as you include a terrain object in the scene, frame rates drop to “unplayable” and none of the kings tweaks or any of his¬†optimisations¬†will bring it back above 20fps again (if anyone thinks differently, please do let me know how you made it work).

So, after cursing for a little while, I accepted my fate and threw away the 30+ hours of work I’d spend getting the terrain just like I wanted it. The replacement is a flat plane which came with the obvious problem of how to create holes. It’s easy enough to add geometry on top of the plane (like the rocks in the screenshot), but when you have objects that extend below the plane (like the crater), it doesn’t really work. I considered custom building the plane geometry at run-time, but was a little concerned with the potentially growing triangle count, so I ended up using a custom shader that allow me to “paint” holes in the plane at run time. While not as pretty as the terrain, it sure is a helluva lot faster.

I’ve done a lot of work on weapons and enemies and am quite close to having something playable – the environment is still a little pale, but since it’s purely for decoration, I want the gameplay done before I start polishing it.

Day15b Day17a

So far the¬†armoury¬†includes 3 automatic sentries: The obligatory mini-gun, a flamethrower and a rocket launcher. The rocket launcher uses the Unity physics engine for rocket movement, creating some funky trajectories. On top of this I’ve added two manually controlled weapons to involve the player a little more in this aspect of the game: The plasma canon is the surgeons tool to take out individual enemies while the not-so-subtle “Artillery” covers and entire area in a mayhem of fire and debris for a short period of time.

I’ve added limited ammo, including clips and reload delays to all weapons. This was not in the original game, but it’s another parameter to adjust when balancing the game later, and I think it will come in handy. I will also add the option to turn sentries on and off to make the most use of whatever ammo you have.

The beasts fall into two categories: Walking and flying – both are done and working, although the flyers still need some tweaking… I have 6 unique beast types in total, 4 walking and 2 flying – some of them will be included in various skins with different attributes.