Recursive delay fiber vs calling a method based on a counter

I was going to paste a lot of my game loop code here, but asterite (and 99% of devs) prefer REDUCED code. So I hope I reduced it enough. :grin:

TICKRATE = 1/25

class Client
  property user_id = 0

  def initialize(@user_id)
    start_looping_fiber
  end

  def start_looping_fiber
    delay(1) {
      pp "I am called once per second! #{user_id} I AM INSIDE A DELAY FIBER!!"

      spawn start_looping_fiber # WE ARE OFF TO THE RACESSSS
    }
  end

  # Called every second
  def update_second(delta)
    pp "I am called once per second! #{user_id}"
  end
end

class Server
  property players = Hash(Int32, Client).new

  def initialize
    players[1] = Client.new(1)
    players[2] = Client.new(2)
    game_loop
  end

  # PRETEND THIS HAS A PROPER DELTA FOR BREVITY SAKE
  def game_loop
    second_counter = 0
    loop do
      second_counter += TICKRATE

      if second_counter > 1
        players.each do |player_id, player_obj|
          player_obj.update_second(TICKRATE)

          if player_id == 1
            players.delete player_id
            player_obj.start_looping_fiber.cancel
            pp "#{player_id} has been deleted"
          end
        end
        second_counter = 0
      end

      sleep TICKRATE
    end
  end
end

Server.new
  • What’s the performance difference between start_looping_fiber, compared to just calling update_second based on a counter?
  • If there is a performance difference or not, which way is more proper? Example, what if you have 1000 players. delay would create 1000 fibers per second, instead of just using .each, and calling the update_second method.

I’ve noticed if I call player_obj.start_looping_fiber.cancel, the fiber still runs. Is that intended?

Depends on how your game scales per player. I’d imagine a Crystal app could run okay on 1000 fibers, but if there are multiple fibers per player, you may start noticing issues at scale.

Fibers are cheap, but they’re not free. They add CPU time for the scheduler to manage them and if you’re spinning them up and letting them die often, they’ll have to be GCed as well, so it’s entirely possible that update_second will be more performant.

To be real, though, you should probably use whichever one makes you happier to work with. Just make sure you make affordances for eliminating the extra fibers if you need to in order to gain some performance, assuming you choose that route.

1 Like

Thanks. I like the update_second way. I’ll probably stick to that. I just recently find out about delay from a SO post by @jhass, and figured I’d see if I could/should incorporate it into my game logic

1 Like

I don’t know about 1000 players because I can’t set ulimit to a high enough value in WSL, but I can confirm that 500 with multiple fibers per player (doing stuff) works without lag even without --release in a fairly involved server.

2 Likes

@Exilor Wow, that’s amazing. Good to know, thanks.

Is that a L2 private server running on Crystal??

Yep.

Do you have a blog post or something?
I would be interested to read about this.

No, sorry, I don’t have a blog or anything of the sort.

Oh nice, I didn’t realize delay was part of the stdlib! I assumed it was a reference to something else in your own code.

I wouldn’t rely on it, it was done in a haste and it might go away in the future.

Erk! I am using this, and actually found it quite usefull.
Any specific reason to remove it?

It wasn’t done by the core team and we merged it in a haste. Maybe it will stay, maybe not.

Hi, i want to fix not working shards, see code like this.

Obviously, the delay was removed.

What is this delay(seconds) { timeout_handler.call } stand for? and what is the alternative approach?

That’s moved to future shard.

2 Likes