AddForce in Coroutine
Hi,
I am developing a game for Android and iOS in which the player taps the screen to gradually move left or right to avoid falling. I decided to implement this requirement with a coroutine that, when the screen is tapped, undergoes a while loop until a maximum horizontal velocity is reached; each loop it adds a force to the rigidbody. I have tested this and it works well on Android when I developed it on my Windows computers. However, I switched the whole project to Mac OS recently to be able to test and build for iOS. On iOS when I tap the screen, a force is applied but apparently much weaker than what it is on my Windows/Android workspace to the point that the player always falls because it cant move fast enough. I have been able to fix this by increasing the move speed of the player when turning on the iOS version. I am not satisfied with this solution because it seems that there is a much deeper bug in my system that will show its head on other devices. I have concluded two possible reasons for this issue.
The coroutine context switching varies on device. This means that the addForce loop will apply differently on each device. Thus, causing the game on some devices to apply the force differently within a given time frame.
This is probably far-fetched, but maybe Mac OS version of Unity applies forces differently. I reasoned this being a possibility because I never came across this issue when I developed the game on Windows while switching between my desktop and laptop (my desktop is way more powerful than the laptop so I would have run into this problem while testing on my laptop. Alas, this only occurs on my Mac test machine, both on running the game within the editor and on a physical iOS device).
Here is the gist of the code I believe is problematic.
void FixedUpdate () {
if( isPlayerControlEnabled ){
if( allowTurn ){
if( moveDirection == Vector3.left ){
StartCoroutine("newTurnLeft");
}else if( moveDirection == Vector3.right ){
StartCoroutine("newTurnRight");
}else if (moveDirection == Vector3.down * 0f){
StartCoroutine("newStopHorizontalMovement");
}
allowTurn = false;
}
}
}
public IEnumerator newTurnLeft(){
while(rigidBody.velocity.x > -maxHorizontalVelocityMagnitude){
rigidBody.AddForce (Vector3.left * moveSpeed);
yield return null;
}
}
public IEnumerator newTurnRight(){
while(rigidBody.velocity.x < maxHorizontalVelocityMagnitude){
rigidBody.AddForce (Vector3.right * moveSpeed);
yield return null;
}
}
public IEnumerator newStopHorizontalMovement(){
if( rigidBody.velocity.x > 0f ){
while( rigidBody.velocity.x > 0f ){
rigidBody.AddForce (Vector3.left * moveSpeed);
yield return null;
}
rigidBody.velocity.Set (0f, 0f, 0f);
}else if( rigidBody.velocity.x < 0f ){
while( rigidBody.velocity.x < 0f ){
rigidBody.AddForce (Vector3.right * moveSpeed);
yield return null;
}
rigidBody.velocity.Set (0f, 0f, 0f);
}
}
Realistically, I feel that the first reason is the problem. Yet, I do not know enough on how Unity handles the physics update system and coroutine calls. I have learned that it is best to apply physics based updates inside the FixedUpdate method so my question boils down to this: can you apply addForce reliably inside a coroutine? Do I have to refactor my movement system so that each addForce call is being called explicitly inside the FixedUpdate method?
Any insight on this is greatly appreciated. Thank you for taking the time to read this.
Just glancing, it looks like a standard frame-rate problem. Lots of explanation/answers about it on this site, mostly involving Time.deltaTime.
Answer by Pengocat · Jan 20, 2017 at 07:14 PM
When using yield return null
you wait for the next update. If you instead use yield return new WaitForFixedUpdate();
it continues after a fixedUpdate has been called. A general comment is that it is not ideal to start a new Coroutine every fixed frame as it involves some overhead. Perhaps start it once and use a bool within the Coroutine instead and leave it running until it is no longer needed.
That did it! I changed the yield return null
to yield return new WaitForFixedUpdate()
and it works perfectly now. So to clarify, any coroutine you start with StartCoroutine will return control to that coroutine on frame update?
I am not sure I understand your question. When a Coroutine yields it resumes depending on whatever yield return method you called. So if you yield return null;
yes then it resumes on the next normal Update but there are many ways to yield, one of them being WaitForFixedUpdate()
Here is a simple example that has one Coroutine that runs as long as the class instance is active.
using System.Collections;
using UnityEngine;
[RequireComponent(typeof(Rigidbody))]
public class Physics$$anonymous$$ovement : $$anonymous$$onoBehaviour
{
public float thrust = 10f;
public bool is$$anonymous$$oveable = true;
private WaitForFixedUpdate waitForFixedUpdate = new WaitForFixedUpdate();
private Coroutine moveCoroutine;
private Rigidbody rb;
private Vector3 direction;
// Awake is called when the script instance is being loaded
private void Awake()
{
rb = GetComponent<Rigidbody>();
}
// This function is called when the object becomes enabled and active
private void OnEnable()
{
moveCoroutine = StartCoroutine($$anonymous$$ove());
}
// This function is called when the behaviour becomes disabled or inactive
private void OnDisable()
{
StopCoroutine(moveCoroutine);
}
// Update is called every frame, if the $$anonymous$$onoBehaviour is enabled
private void Update()
{
if (Input.Get$$anonymous$$eyDown($$anonymous$$eyCode.UpArrow))
{
direction = Vector3.up;
}
if (Input.Get$$anonymous$$eyDown($$anonymous$$eyCode.DownArrow))
{
direction = Vector3.down;
}
if (Input.Get$$anonymous$$eyDown($$anonymous$$eyCode.LeftArrow))
{
direction = Vector3.left;
}
if (Input.Get$$anonymous$$eyDown($$anonymous$$eyCode.RightArrow))
{
direction = Vector3.right;
}
if (Input.Get$$anonymous$$eyDown($$anonymous$$eyCode.Backspace))
{
is$$anonymous$$oveable = false;
}
if (Input.Get$$anonymous$$eyDown($$anonymous$$eyCode.Return))
{
is$$anonymous$$oveable = true;
}
}
private IEnumerator $$anonymous$$ove()
{
while (true)
{
if (is$$anonymous$$oveable)
{
rb.AddForce(direction * thrust);
}
yield return waitForFixedUpdate;
}
}
}