• PID Control of Physics Bodies

Math and Physics

This article discusses an approach to making physics bodies rotate smoothly, whether they are stationary or actively moving. I used cocos2d-x and Box2D, but the basic approach will work for any physics body (even a 3D one if you are trying to rotate in a 2D plane). The approach uses a Proportional-Integral-Derivative (PID) control loop on the angle of the body to apply torque to it in a well controlled and predictable manner. Two examples of using the approach are shown in the video, one for a missile that can only move in the direction it is facing and another for an "Entity" like a game character that moves independent of its facing direction. The video below shows this in action. You don't have to watch the whole video to get the idea, but there is a lot in there to see...

Facing Direction

Whether your game is in 2-D or 3-D, you often have the need to make an object "turn" to face another direction. This could be a character's walking direction as they are moving, the direction they are shooting while sitting crouched, the direction a missile is flying in, the direction a car is racing towards, etc. This is the job of the "character controller", the piece of code in your system responsible for the basic "movement" operations that a character must undergo (seek, turn, arrive, etc.). Building games that use physics engines is a lot of fun and adds a level of realism to the game that can dramatically improve the gameplay experience. Objects collide, break, spin, bounce, and move in more realistic ways. Moving in more realistic ways is not what you usually think about, though, when you think about facing direction. Your usual concern is something like "The character needs to turn to face left in 0.5 seconds." From the standpoint of physics, this means you want to apply torque to make it turn 90? left in 0.5 seconds. You want it to stop exactly on the spot. You don't want to worry about things like angular momentum, which will tend to keep it turning unless you apply counter torque. You really don't want to think about applying counter torque to make it stop "on a dime". Box2D will allow you to manually set the position and angle of a body. However, if you manually set the position and angle of a physics body in every frame, it can interfere (in my experience) with the collision response of the physics engine. Most important of all, this is a physics engine. You should be using it as such to make bodies move as expected. Our goal is to create a solution to change the facing direction of the body by applying turning torque to it. NOTE THAT PID CONTROL LOOPS CAN BE USED FOR LOTS OF OTHER THINGS. This example just happens to be the one I chose. If we decouple the problem of "how it turns" from "how it moves", we can use the same turning solution for other types of moving bodies where the facing direction needs to be controlled. For this article, we are considering a missile that is moving towards its target.
Here, the missile is moving in a direction and has a given velocity. The angle of the velocity is measured relative to the x-axis. The "facing direction" of the missile is directly down the nose and the missile can only move forward. We want to turn it so that it is facing towards the target, which is at a different angle. For the missile to hit the target, it has to be aiming at it. Note that if we are talking about an object that is not moving, we can just as easily use the angle of the body relative to the x-axis as the angle of interest.

Feedback Control Systems 101

The basic idea behind a control system is to take the difference of "what you want the value to be" and "what the value is" and adjust your input to the system so that, over time, the system converges to your desired value. From this wikipedia article:
A familiar example of a control loop is the action taken when adjusting hot and cold faucets (valves) to maintain the water at a desired temperature. This typically involves the mixing of two process streams, the hot and cold water. The person touches the water to sense or measure its temperature. Based on this feedback they perform a control action to adjust the hot and cold water valves until the process temperature stabilizes at the desired value.
There is a huge body of knowledge in controls system theory. Polynomials, poles, zeros, time domain, frequency domain, state space, etc. It can seem daunting to the uninitiated. It can seem daunting to the initiated as well! That being said, while there are more "modern" solutions to controlling the facing direction, we're going to stick with PID Control. PID control has the distinct advantages of having only three parameters to "tune" and a nice intuitive "feel" to it.

PID Control

Let's start with the basic variable we want to "control", the difference between the angle we want to be facing and the angle of the body/velocity:
$$e(t) = desired - actual$$
Here, $$e(t)$$ is the "error". We want to drive the error to 0. We apply forces to the body to make it turn in a direction to make it turn so that it moves $$e(t)$$ towards 0. To do this, we create a function $$f(.)$$, feed $$e(t)$$ into it, and apply torque to the body based on it. Torque makes bodies turn:
$$torque(t) = I * f(e(t)), I \equiv Angular Inertia$$

Proportional Feedback

The first and most obvious choice is to apply a torque that is proportional to the $$e(t)$$ itself. When the error is large, large force is applied. When the error is small, small force is applied. Something like:
$$f(e(t)) = K_p * e(t)$$
And applying the torque with just proporational feedback will work. Somewhat. The problem is that when the error is small, the corrective torque is small as well. So as the body is turning $$e(t)$$ gets small (nearing the intended angle), the retarding torque is small also. So the body overshoots and goes past the angle intended. Then you start to swing back and eventually oscillate to a steady state. If the value $$K_p$$ is not too large, the oscillation should settle into "damped" (exponentially decaying) sinusoid, dropping a large amount each oscillation (stable solution). It may also spiral off towards infinity (unstable solution), or just oscillate forever around the target point (marginally stable solution). If you reduce $$K_p$$ so that it is not moving so fast, then when $$e(t)$$ is large, you don't have a lot of driving force to get moving. A pure proportional error also has a bias (Steady State Error) that keeps the final output different from the input. The error is a function of $$K_p$$ of the form:
$$Steady State Error = \frac{desired}{ [1 + constant * K_p]}$$
So increasing the $$K_p$$ value makes the bias smaller (good). But this will also make it oscillate more (bad).

Integral Feedback

The next term to add is the integral term, the "I" in PID:
$$f(e(t)) = K_p * e(t) + \int\limits_{-\infty}^{now} K_i * e(t)$$
For each time step, if $$e(t)$$ has a constant value, the integral term will work to counter it:
• If direction to the target suddenly changes a small amount, then over each time step, this difference will build up and create turning torque.
• If there is a bias in the direction (e.g. Steady State Error), this will accumulate over the time steps and be countered.
The integral term works to counter any constant offset being applied to the output. At first, it works a little but over time, the value accumulates (integrates) and builds up, pushing more and more as time passes. We don't have to calculate the actual integral. We probably don't want to anyway since it stretches back to $$-\infty$$ and an error back in the far past should have little effect on our near term decisions. We can estimate the integral over the short term by summing the value of $$e(t)$$ over the last several cycles and multiplying by the time step (Euler Integration) or some other numerical technique. In the code base, the Composite Simpson's Rule technique was used.

Derivative Feedback

Most PID controllers stop at the "PI" version. The proportional part gets the output swinging towards the input and the integral part knocks out the bias or any steady external forces that might be countering the proportional control. However, we still have oscillations in the output response. What we need is a way to slow down the rotation as the body is heading towards the target angle. The proportional and integral components work to push towards it. By looking at the derivative of $$e(t)$$, we can estimate its value in the near term and apply force to drive it towards not changing. This is a counter-torque to the proportional and integral components:
$$f(e(t)) = K_p * e(t) + \int\limits_{-\infty}^{now} K_i * e(t) dt + K_d * \frac{de(t)}{dt}$$
Consider what happens when $$e(t)$$ is oscillating. Its behavior is like a sine function. The derivative of this is a cosine function and its maximum occurs when sin(e(t)) = 0. That is to say, the derivative is largest when $$e(t)$$ is swinging through the position we want to achieve. Conversely, when the oscillation is at the edge, about to change direction, its rate of change switches from positive to negative (or vice versa), so the derivative is smallest (minimum). So the derivative term will apply counter torque hardest when the body is swinging through the point we want to be at, countering the oscillation, and least when we are at either edge of the "swing". Just like the integral, the derivative can be estimated numerically. This is done by taking differences over the last several $$e(t)$$ values (see the code).
Using derivative control is not usually a good idea in real control systems. Sensor noise can make it appear as if $$e(t)$$ is changing rapidly back and forth, causing the derivative to spike back and forth with it. However, in our case, unless we are looking at a numerical issue, we should not have a problem.

Classes and Sequences

Because we are software minded, whatever algorithm we want to use for a PID controller, we want to wrap it into a convenient package, give it a clean interface, and hide everything except what the user needs. This needs to be "owned" by the entity that is doing the turning.
The MovingEntityInterface represents a "Moving Entity". In the case of this demo, it can be an entity like a Missile, which moves forward only, or a "character", which can turn while moving. While they have different methods internally for "applying thrust" they both have nearly identical methods for controlling turning. This allows the implementation of a "seek" behavior tailored more to the entity type. The interface itself is generic so that the MainScene class can own an instance and manipulate it without worrying about what type it is. The PIDController class itself has this interface:  /******************************************************************** * File : PIDController.h * Project: Interpolator * ******************************************************************** * Created on 10/13/13 By Nonlinear Ideas Inc. * Copyright (c) 2013 Nonlinear Ideas Inc. All rights reserved. ******************************************************************** * This software is provided 'as-is', without any express or implied * warranty. In no event will the authors be held liable for any * damages arising from the use of this software. * * Permission is granted to anyone to use this software for any * purpose, including commercial applications, and to alter it and * redistribute it freely, subject to the following restrictions: * * 1. The origin of this software must not be misrepresented; you must * not claim that you wrote the original software. If you use this * software in a product, an acknowledgment in the product * documentation would be appreciated but is not required. * 2. Altered source versions must be plainly marked as such, and * must not be misrepresented as being the original software. * 3. This notice may not be removed or altered from any source * distribution. */ #ifndef __Interpolator__PIDController__ #define __Interpolator__PIDController__ #include "CommonSTL.h" #include "MathUtilities.h" /* This class is used to model a Proportional- * Integral-Derivative (PID) Controller. This * is a mathemtical/control system approach * to driving the state of a measured value * towards an expected value. * */ class PIDController { private: double _dt; uint32 _maxHistory; double _kIntegral; double _kProportional; double _kDerivative; double _kPlant; vector _errors; vector _outputs; enum { MIN_SAMPLES = 3 }; /* Given two sample outputs and * the corresponding inputs, make * a linear pridiction a time step * into the future. */ double SingleStepPredictor( double x0, double y0, double x1, double y1, double dt) const { /* Given y0 = m*x0 + b * y1 = m*x1 + b * * Sovle for m, b * * => m = (y1-y0)/(x1-x0) * b = y1-m*x1 */ assert(!MathUtilities::IsNearZero(x1-x0)); double m = (y1-y0)/(x1-x0); double b = y1 - m*x1; double result = m*(x1 + dt) + b; return result; } /* This funciton is called whenever * a new input record is added. */ void CalculateNextOutput() { if(_errors.size() < MIN_SAMPLES) { // We need a certain number of samples // before we can do ANYTHING at all. _outputs.push_back(0.0); } else { // Estimate each part. size_t errorSize = _errors.size(); // Proportional double prop = _kProportional * _errors[errorSize-1]; // Integral - Use Extended Simpson's Rule double integral = 0; for(uint32 idx = 1; idx < errorSize-1; idx+=2) { integral += 4*_errors[idx]; } for(uint32 idx = 2; idx < errorSize-1; idx+=2) { integral += 2*_errors[idx]; } integral += _errors[0]; integral += _errors[errorSize-1]; integral /= (3*_dt); integral *= _kIntegral; // Derivative double deriv = _kDerivative * (_errors[errorSize-1]-_errors[errorSize-2]) / _dt; // Total P+I+D double result = _kPlant * (prop + integral + deriv); _outputs.push_back(result); } } public: void ResetHistory() { _errors.clear(); _outputs.clear(); } void ResetConstants() { _kIntegral = 0.0; _kDerivative = 0.0; _kProportional = 0.0; _kPlant = 1.0; } PIDController() : _dt(1.0/100), _maxHistory(7) { ResetConstants(); ResetHistory(); } void SetKIntegral(double kIntegral) { _kIntegral = kIntegral; } double GetKIntegral() { return _kIntegral; } void SetKProportional(double kProportional) { _kProportional = kProportional; } double GetKProportional() { return _kProportional; } void SetKDerivative(double kDerivative) { _kDerivative = kDerivative; } double GetKDerivative() { return _kDerivative; } void SetKPlant(double kPlant) { _kPlant = kPlant; } double GetKPlant() { return _kPlant; } void SetTimeStep(double dt) { _dt = dt; assert(_dt > 100*numeric_limits::epsilon());} double GetTimeStep() { return _dt; } void SetMaxHistory(uint32 maxHistory) { _maxHistory = maxHistory; assert(_maxHistory >= MIN_SAMPLES); } uint32 GetMaxHistory() { return _maxHistory; } void AddSample(double error) { _errors.push_back(error); while(_errors.size() > _maxHistory) { // If we got too big, remove the history. // NOTE: This is not terribly efficient. We // could keep all this in a fixed size array // and then do the math using the offset from // the beginning and module math. But this // gets complicated fast. KISS. _errors.erase(_errors.begin()); } CalculateNextOutput(); } double GetLastError() { size_t es = _errors.size(); if(es == 0) return 0.0; return _errors[es-1]; } double GetLastOutput() { size_t os = _outputs.size(); if(os == 0) return 0.0; return _outputs[os-1]; } virtual ~PIDController() { } };  This is a very simple class to use. You set it up calling the SetKXXX functions as needed, set the time step for integration, and call AddSample(...) each update cycle with the error term. Looking at the Missile class, which owns an instance of this, the step update (called in Update) looks like this:  void ApplyTurnTorque() { Vec2 toTarget = GetTargetPos() - GetBody()->GetPosition(); float32 angleBodyRads = MathUtilities::AdjustAngle(GetBody()->GetAngle()); if(GetBody()->GetLinearVelocity().LengthSquared() > 0) { // Body is moving Vec2 vel = GetBody()->GetLinearVelocity(); angleBodyRads = MathUtilities::AdjustAngle(atan2f(vel.y,vel.x)); } float32 angleTargetRads = MathUtilities::AdjustAngle(atan2f(toTarget.y, toTarget.x)); float32 angleError = MathUtilities::AdjustAngle(angleBodyRads - angleTargetRads); _turnController.AddSample(angleError); // Negative Feedback float32 angAcc = -_turnController.GetLastOutput(); // This is as much turn acceleration as this // "motor" can generate. if(angAcc > GetMaxAngularAcceleration()) angAcc = GetMaxAngularAcceleration(); if(angAcc < -GetMaxAngularAcceleration()) angAcc = -GetMaxAngularAcceleration(); float32 torque = angAcc * GetBody()->GetInertia(); GetBody()->ApplyTorque(torque); } 

Nuances

If you look carefully at the video, there is a distinct difference in the way path following works for the missile vs. the character (called the MovingEntity in the code). The missile can overshoot the path easily, especially when its maximum turn rate is reduced. The MovingEntity always moves more directly towards the points because it is using a "vector feedback" of its position vs. the target position to adjust its velocity. This is more like a traditional "seek" behavior than the missile. I have also, quite deliberately, left out a bit of key information on how to tune the constants for the PID controller. There are numerous articles on Google for how to tune a PID control loop, and I have to leave something for you to do, after all. You will also note that the default value for _dt, the time step, is set for 0.01 seconds. You can adjust this value to match the timestep you actually intend to use, but there will be tradeoffs in the numerical simulation (error roundoff, system bandwidth concerns, etc.) that you will encounter. In practice, I use the same controller with the same constants across multipe sized physical entities without tweaking and the behavior seems realistic enough (so far) that I have not had to go and hunt for minor tweaks to parameters. Your mileage may vary. The source code for this, written in cocos2d-x/C++, can be found on github here. The PIDController class has no dependencies other than standard libraries and should be portable to any system.

Article Update Log

10 Dec 2014: Deconflated torque and force and other textual updates to make article more clear. 3 Nov 2014: Text correction in "Integral" section. 29 Oct 2014: Initial release

Report Article

User Feedback

I implemented my seek behavior based off of the classic boids steering articles.

Here is my seek code (C# / XNA):

public virtual Vector3 Seek(Vector3 target)
{
Vector3 desiredVelocity = Vector3.Normalize(Position - target) * m_topSpeed.Current;
Vector3 steerForce = (desiredVelocity - m_velocity);
return -steerForce;
}

Physical.Update(worldTime)
{

...

m_acceleration = (m_steerForce / m_mass) + (m_gravityForce * (float)(worldTime.ElapsedWorldTime.TotalSeconds)); //F = ma -> a = F/m
m_velocity += m_acceleration; //velocity is the change in acceleration over time

//don't move faster than our top speed
if (m_velocity != Vector3.Zero)
{
if (m_velocity.Length() > m_topSpeed.Current)
{
m_velocity.Normalize();
m_velocity *= m_topSpeed.Current;
}
}

Vector3 moveStep = m_velocity * (float)worldTime.ElapsedWorldTime.TotalSeconds;
Position += moveStep;      //position is the change in velocity over time

...

}



It works for 3D, so switching to 2D is pretty straight forward as well. I didn't really implement rotational turn rates though, its kind of implied based off of the objects mass and steering force.

Share on other sites

"We can estimate the derivative over the short term by summing the value "

do you mean integral here?

interesting

Share on other sites

For Slayermin

I didn't know about Craig Reynolds and "boids" until I read Programming Game AI By Example.  I did use the techniques from those articles and the Buckland book in some of my earlier work.  Once I found a solid 2D physics engine, it seemed prudent to move up to something that was efficient and tested so I could focus on the AI and worry less about physics.

Of course, this came with a tradeoff.  In your code, you are integrating to get the position of the body.  The orientation can be aligned with the velocity or iterate towards it over time or not face it at all.  That depends on what you are modeling (spaceship, missile, strafing, etc.).  But in my case, while I got the lift of the code for the physics, I found that I could not set the rotation of the bodies arbitrarily every frame.  When you have a physics engine involved with bodies and fixtures and rotational inertia and temporal coherence used to predict future collisions, I have found that setting the rotation/position manually can muck up the physics.

There is a video for a demo I did (here).  The spiders crawl around the asteroids using...some techniques I'll do another article about.  Regardless, one of the ways I had them stick early on was to try and adjust their position each frame so that they pointed along the normal vector to the bounding hull at their position.  That is to say, forcibly changing their position and orientation each frame.  This caused weirdness, at least in an earlier version of the Box2D engine, specifically with collision response.  Since I don't like weirdness, I found another technique that appears to work and allows me to leverage the physics engine.

Similar problems occurred for "free roaming" bodies when I tried to manually change the position (I had a teleporter and I was just changing the transform of the body) continuously.

So in general, I switched to using a control loop to drive the orientation of the body to face the position I wanted.  I recognize that having physics bodies is both a good and a bad:  good reactions, but generally, difficult character controllers.  If you get into the same position for some reason, perhaps this article will give you some options for getting the behavior you want.

Share on other sites

"We can estimate the derivative over the short term by summing the value "

do you mean integral here?

interesting

Good Find! Fixed.

Share on other sites

My coffee hasn't quite kicked in yet this morning, so I'm fighting through some brain fog to understand exactly what you're saying (so excuse my obtuseness). From what I gather, you're saying that the physical bodies in your system have to move in the direction they're oriented towards? If so, that seems like an unhappy constraint to work around.

Anyways, my initial reaction when reading the article was, "Whoa, this is way too complicated. Too many equations and calculus. Didn't I implement seek behavior in three lines of code? Maybe all of this is unnecessary... well, what does this solve / account for which my solution doesn't? Probably rotational momentum. But if I treat my angular momentum just like my velocity vector, I can apply changes to my angular momentum by applying a rotational acceleration to the angular velocity. The only hard part would be figuring out how much of a rotational acceleration I'd need to apply to face a desired direction, but my game doesn't need that granular level of physical behavior. What I have works for me and there are fewer equations and code for me to mess up, so it's probably better. I should share it."

Share on other sites

My coffee hasn't quite kicked in yet this morning, so I'm fighting through some brain fog to understand exactly what you're saying (so excuse my obtuseness). From what I gather, you're saying that the physical bodies in your system have to move in the direction they're oriented towards? If so, that seems like an unhappy constraint to work around.

Anyways, my initial reaction when reading the article was, "Whoa, this is way too complicated. Too many equations and calculus. Didn't I implement seek behavior in three lines of code? Maybe all of this is unnecessary... well, what does this solve / account for which my solution doesn't? Probably rotational momentum. But if I treat my angular momentum just like my velocity vector, I can apply changes to my angular momentum by applying a rotational acceleration to the angular velocity. The only hard part would be figuring out how much of a rotational acceleration I'd need to apply to face a desired direction, but my game doesn't need that granular level of physical behavior. What I have works for me and there are fewer equations and code for me to mess up, so it's probably better. I should share it."

The bodies only have to move in the direction of the velocity if you want them to.  In my situation, I am applying two kinds of forces using the physics engine, rotational and directional to the center of mass.  Using the PID to control the rotational force allows you to have tight control over the facing direction without worrying (too much) about how much force to apply.

In your implementation, you have complete control over you body, so you absolutely can set the rotation independently as well.  You don't even need to do rotational inertia...you can do a cubic ease in/out for the rotation to a direction and it will probably look pretty good. However, when you put a physics engine in play to get all those complex collisions, ropes, joints, cell space partition, etc., I have found that manually controlling the rotation is a hassle.  So I do it this way.  And it seems to work out pretty well.

Just to be clear, I'm not saying your way is wrong.  It is perfectly fine.  For the level of detail you want and the physical reality you want to have.  Simple is good.  But that approach does not work as I want it to for my situation.

Share on other sites

A great article that explains clearly and precisely what a PID controller does.

I have tried to create PID controllers before and always never quite got them to work because I simply did not understand the maths. Now with your code I can control my physical bodies much more accurately and predictably.

Thanks ^_^

Share on other sites

A great article that explains clearly and precisely what a PID controller does.

I have tried to create PID controllers before and always never quite got them to work because I simply did not understand the maths. Now with your code I can control my physical bodies much more accurately and predictably.

Thanks

Share on other sites

First - glad to see an article integrating physics principles into software.

As it's a relatively long article, I'm still working through it. Here's some comments from a read-through.

Generic - you mix the terms "force" and "torque" a lot. I like that you go into the physics of the situation, but, as you've bitten off that chunk to chew, you should be a bit more accurate when using those terms. That is, as your primary example describes applying torque to the CG, a torque at the CG doesn't result in any forces.

In the paragraph beginning "And this would work. Somewhat..." - You use the pronoun "it" a lot, and what "it" refers to isn't always clear, and appears to refer to different things. You might clarify the reference. E.g.,

"If the Kp is not too large, it should settle.." -> "it" == direction? torque?

"It may also spiral off towards infinity.." -> "it" == the object?

"the retarding force is small.." - Isn't the point that there isn't any retarding torque (not force!) until the error term changes sign? And that's why ".. the body overshoots.." IMHO, if you provide an explanation of why it overshoots, that will help lead into the rest of the article. Until the error term changes sign (output exceeds setpoint), the proportional contribution will not provide any slowing of the rotation. I.e., until the sign of the error changes, proportional will continue to provide angular acceleration toward the setpoint. Something is needed to slow or damp the rotation as it approaches setpoint. Perhaps add something along those lines?

In the section Derivative: - " What we need is a way to slow down as the body is heading towards the target angle." Minor point - rather than "slow down" (which is more of a linear motion term,) it might be clearer to say something like "a way to slow/reduce the (rate of) rotation.."

"This is a counter-force to the proportional and integral components:" and "So the derivative term will apply counter force" - not a counter-force, really. Maybe, "reduces the contributions?" Can't think of a good description to suggest, except it's not a force. As mentioned above, you've chosen to bite off the physics to chew (and that's a good thing), you should be more consistent.

"when the oscillation is at the edge," - "edge" doesn't seem to be a well-defined term here. "turning point" or "slope is about to change sign," maybe?

Classes and Sequences - "This needs to be "owned" by the entity that is doing the turning." What needs to be owned? And why? IMO, expanding just a little on those statements would be a nice enhancement.

Nuances - "when its turn rate is turned down" Really? turn rate turned down? ;-) How 'bout "reduced" or similar?

One more comment from a life-time instrumentation engineer: "You can adjust this value to match the timestep you actually intend to use," - PID controllers (real ones) commonly have an adjustment called "resets-per-second." I.e., how often to recalculate. It might make a good additional to the PID settings to limit unneeded run-time calculations. I.e., slow, small-MOI objects can be updated less frequently than fast, large-MOI objects. Just something to consider.

Share on other sites

I applied an update for most of these comments.  Good feedback.  Thanks!

Share on other sites

You've got a good article going here, Fuzzy Bunny Slippers. A consideration: as the emphasis of the article is a PID implementation, heavy on math and physics, and the PID can be applied to animate and inanimate objects alike, perhaps a better forum for the article is Math and Physics, vs. Artificial Intelligence?

Share on other sites

You've got a good article going here, Fuzzy Bunny Slippers. A consideration: as the emphasis of the article is a PID implementation, heavy on math and physics, and the PID can be applied to animate and inanimate objects alike, perhaps a better forum for the article is Math and Physics, vs. Artificial Intelligence?

Please disregard my previous comment on this...I was cross-wired thinking you were referring to my article on "throwing a winning pass".  I agree, this article probably belongs in "math and physics".

Create an account

Register a new account

• Game Developer Survey

We are looking for qualified game developers to participate in a 10-minute online survey. Qualified participants will be offered a \$15 incentive for your time and insights. Click here to start!

• 0
• 0
• 0
• 4

• 11
• 15
• 21
• 26
• 11
×