+1 (614) 348-7474


Free in USA


From 10:00 to 21:00


From 10:00 to 21:00

Fill out the brief

Online request


Code optimization methods on the example of River Raft

Crocoapps editorial

Crocoapps editorial

Reading time: 2 minutes

Optimization is a very important step in game development. Every game needs optimization. RiverRaft game is a sophisticated river rafting simulator and the target platform is Android where not everyone has powerful enough devices. The game has dynamic water. An object that simulates the behavior of water in the game, repeating its physics. Also a script that simulates a plane mesh change, emulating waves. A script that is responsible for the physics of a floating object in waves.

So, let's get started. The first method is getting rid of unnecessary calculations in the Update method. Since Update performs a rendering every frame and this is too often for complex calculations, which in itself entails a decrease in the quality of frames per second.

Let's look at all this using the Floating Object script as an example. Here we calculate most of the values ​​in the Start method. And we keep them in mind. Thus, we get rid of the unwanted “dividing every frame”.

void Start()
  objectVolume = this.rigidbody.mass / this.density;
  voxelsCountForEachAxis = Mathf.RoundToInt(1f / this.normalizedVoxelSize);

  _bounds = this.collider.bounds;
  voxelHeight = _bounds.size.y * this.normalizedVoxelSize;
  voxelHeightHalf = voxelHeight * 0.5f;
  voxelOneHeightDel = (float)1 / (float)voxelHeight;

protected virtual void FixedUpdate()
  if (this.water != null && this.voxels.Length > 0)

    float submergedVolume = 0f;

    for (int i = 0; i < this.voxels.Length; i++)
      Vector3 worldPoint = this.transform.TransformPoint(this.voxels[i]);

      float waterLevelSum = 0;
      waterCollides.ForEach(x => { waterLevelSum += x.GetWaterLevel(worldPoint); });
      float waterLevel = waterLevelSum / waterCollides.Count;
      float deepLevel = waterLevel - worldPoint.y + voxelHeightHalf; // How deep is the voxel

      // 0 - voxel is fully out of the water, 1 - voxel is fully submerged
      float submergedFactor = Mathf.Clamp(deepLevel * voxelOneHeightDel, 0f, 1f);
      submergedVolume += submergedFactor;

      Vector3 surfaceNormal = Vector3.zero;
      waterCollides.ForEach(x => { surfaceNormal += x.GetSurfaceNormal(worldPoint); });
      surfaceNormal = surfaceNormal / waterCollides.Count;

      Vector3 newRotation = Vector3.zero;
      Quaternion surfaceRotation = Quaternion.FromToRotation(newRotation, surfaceNormal);
      surfaceRotation = Quaternion.Slerp(surfaceRotation, Quaternion.identity, submergedFactor);

      Vector3 finalVoxelForce = surfaceRotation * (forceAtSingleVoxel * submergedFactor);
      this.rigidbody.AddForceAtPosition(finalVoxelForce, worldPoint);

      Debug.DrawLine(worldPoint, worldPoint + finalVoxelForce.normalized, Color.blue);

    submergedVolume = submergedVolume * voxelLength; // 0 - object is fully out of the water, 1 - object is fully submerged

    this.rigidbody.drag = Mathf.Lerp(this.initialDrag, this.dragInWater, submergedVolume);
    this.rigidbody.angularDrag = Mathf.Lerp(this.initialAngularDrag, this.angularDragInWater, submergedVolume);

Naturally, we got rid of extra calculations that we don't need, namely waterCollides, newRotation, waterLevel

The input and output parameters for these expressions have the value = 0. But still, the calculations take place and this zero must be obtained somehow. Therefore, it would be logical to get rid of these calculations.

Also in the next method, we replace division with multiplication, with a cached number. Which makes life and the life of the processor much easier for us.

private void CalculateMaxBuoyancyForce()
  Vector3 maxBuoyancyForce = this.water.Density * objectVolume * -Physics.gravity;
  forceAtSingleVoxel = maxBuoyancyForce * voxelLength;

This way we will be able to increase FPS by 10 points.

WaterVolume script. We also get rid of unnecessary calculations. Thus, we increase the FPS by another 5 points.

public float GetWaterLevel(Vector3 worldPoint)
  Vector3[] meshPolygon = this.GetSurroundingTrianglePolygon(worldPoint);

  if (meshPolygon != null)
    Vector3 planeV1 = meshPolygon[1] - meshPolygon[0];
    Vector3 planeV2 = meshPolygon[2] - meshPolygon[0];
    Vector3 planeNormal = Vector3.Cross(planeV1, planeV2).normalized;

    if (planeNormal.y < 0f)
      planeNormal *= -1f;

    // Plane equation
    float yOnWaterSurface = (-(worldPoint.x * planeNormal.x) - (worldPoint.z * planeNormal.z) + Vector3.Dot(meshPolygon[0], planeNormal)) / planeNormal.y;

    return yOnWaterSurface;

  return this.transform.position.y;

And finally, the WaterWaves script: a script that controls the height of the waves and the movement of water up / down along the Y axis, since we are abandoning this function in favor of workable thresholds, we do not need this function and it makes no sense to calculate it once again.

protected virtual void Update()
  for (var i = 0; i < this.vertices.Length; i++)
    var vertex = this.baseVertices[i];

    if (speed != 0 && height != 0)
      vertex.y += Mathf.Sin(Time.time * this.speed + this.baseVertices[i].x + this.baseVertices[i].y + this.baseVertices[i].z) * chacheValue;

    this.vertices[i] = vertex;

  this.mesh.vertices = this.vertices;

When carrying out these actions, we increased the FPS by a total of 25 points. As a result, we have only with these changes 40 fps is already sufficient for the game. But this is not enough. The next article will talk about optimizing locations to increase fps.


Crocoapps editorial

Crocoapps editorial