Ticking

Implementing Multi-threaded Ticking

Is it possible to implement a multi-threaded ticking system for better performance on multi-core processors?

Abstract art representing computer programming

Yes, it's definitely possible to implement a multi-threaded ticking system to take advantage of multi-core processors. This can significantly improve performance, especially for games with a large number of objects or complex simulations.

However, it also introduces challenges related to synchronization and data races. Here's a more detailed look at how you might implement this:

Basic Multi-threaded Ticking

Let's start with a basic implementation that divides the work among multiple threads:

#include <atomic>
#include <mutex>
#include <thread>
#include <vector>

class GameObject {
public:
  virtual void Tick() = 0;
};

class World {
public:
  void AddObject(
    std::unique_ptr<GameObject> obj) {
    std::lock_guard<std::mutex> lock(
      objectsMutex);
    objects.push_back(std::move(obj));
  }

  void TickAll() {
    const size_t numThreads =
      std::thread::hardware_concurrency();
    std::vector<std::thread> threads;

    for (size_t i = 0; i < numThreads; ++i) {
      threads.emplace_back(
        [this, i, numThreads](){
          for (size_t j = i; j < objects.size();
               j += numThreads) {
            objects[j]->Tick();
          }
        });
    }

    for (auto& thread : threads) {
      thread.join();
    }
  }

private:
  std::vector<std::unique_ptr<GameObject>> objects;
  std::mutex objectsMutex;
};

This approach divides the objects among the available threads, with each thread updating a subset of the objects.

Job System

For more flexibility, you can implement a job system that allows for dynamic distribution of work:

#include <condition_variable>
#include <queue>

class JobSystem {
public:
  JobSystem(size_t numThreads) : shouldStop(
    false) {
    for (size_t i = 0; i < numThreads; ++i) {
      threads.emplace_back([this]{
        WorkerThread();
      });
    }
  }

  ~JobSystem() {
    {
      std::unique_lock<std::mutex> lock(
        queueMutex);
      shouldStop = true;
    }
    condition.notify_all();
    for (auto& thread : threads) {
      thread.join();
    }
  }

  void AddJob(std::function<void()> job) {
    {
      std::unique_lock<std::mutex> lock(
        queueMutex);
      jobs.push(std::move(job));
    }
    condition.notify_one();
  }

private:
  void WorkerThread() {
    while (true) {
      std::function<void()> job;
      {
        std::unique_lock<std::mutex> lock(
          queueMutex);
        condition.wait(lock, [this]{
          return !jobs.empty() || shouldStop;
        });
        if (shouldStop && jobs.empty()) {
          return;
        }
        job = std::move(jobs.front());
        jobs.pop();
      }
      job();
    }
  }

  std::queue<std::function<void()>> jobs;
  std::vector<std::thread> threads;
  std::mutex queueMutex;
  std::condition_variable condition;
  bool shouldStop;
};

class World {
public:
  World() : jobSystem(
    std::thread::hardware_concurrency()) {}

  void TickAll() {
    std::atomic<size_t> completedJobs(0);
    const size_t totalJobs = objects.size();

    for (auto& obj : objects) {
      jobSystem.AddJob([&obj, &completedJobs]{
        obj->Tick();
        ++completedJobs;
      });
    }

    while (completedJobs < totalJobs) {
      // Wait for all jobs to complete
    }
  }

private:
  std::vector<std::unique_ptr<GameObject>> objects;
  JobSystem jobSystem;
};

This job system allows for more dynamic work distribution and can be used for other parallel tasks in your game as well.

Handling Dependencies

If your game objects have dependencies, you'll need to be careful about the order of updates:

class GameObject {
public:
  virtual void Tick() = 0;

  virtual std::vector<GameObject*>
    GetDependencies() const { return {}; }
};

class World {
public:
  void TickAll() {
    std::unordered_set<GameObject*> completed;
    std::mutex completedMutex;

    std::function<void(GameObject*)> TickObject
      = [&](GameObject* obj){
      for (auto* dep : obj->GetDependencies()) {
        {
          std::lock_guard<std::mutex> lock(
            completedMutex);
          if (completed.find(dep) == completed.
            end()) {
            jobSystem.AddJob([dep, &TickObject]{
              TickObject(dep);
            });
            return;
          }
        }
      }

      obj->Tick();

      {
        std::lock_guard<std::mutex> lock(
          completedMutex);
        completed.insert(obj);
      }
    };

    for (auto& obj : objects) {
      jobSystem.AddJob([&obj, &TickObject]{
        TickObject(obj.get());
      });
    }

    // Wait for all jobs to complete
  }

private:
  std::vector<std::unique_ptr<GameObject>>
  objects;
  JobSystem jobSystem;
};

This approach ensures that objects are only ticked after their dependencies have been updated, even in a multi-threaded environment.

Remember that multi-threaded programming introduces complexity and potential for bugs like race conditions and deadlocks. Always use appropriate synchronization mechanisms, and thoroughly test your multi-threaded code.

Also, be aware that the overhead of thread management can sometimes outweigh the benefits for small numbers of objects, so profile your game to ensure you're actually gaining performance.

Answers to questions are automatically generated and may not have been reviewed.

sdl2-promo.jpg
Part of the course:

Game Dev with SDL2

Learn C++ and SDL development by creating hands on, practical projects inspired by classic retro games

Free, unlimited access

This course includes:

  • 55 Lessons
  • 100+ Code Samples
  • 91% Positive Reviews
  • Regularly Updated
  • Help and FAQ
Free, Unlimited Access

Professional C++

Comprehensive course covering advanced concepts, and how to use them on large-scale projects.

Screenshot from Warhammer: Total War
Screenshot from Tomb Raider
Screenshot from Jedi: Fallen Order
Contact|Privacy Policy|Terms of Use
Copyright © 2024 - All Rights Reserved