July 6th, 2018 (back)

RAPTR - Fluid Split Screen Local Multiplayer

Fluid Split Screen Multiplayer

I worked out a system for doing a bit more fluid of a split screen multiplayer. I want to reward players for being nearby one another. So, without any hesitation, check this out:

The relevant code this section is going over is here.

Clipping and Viewports

This is really not too painful at all. It just took a bit thinking about how I wanted to go about it. The idea is to allocate 1/n of the available screen space to each player and check if another player is within that area. If they are, then merge the rectangles and now those players share 2/n. Repeat and so on. We ultimately end up forming a bunch of clipping and viewport rectangles to pass to the renderer.

First, sort the entities

The camera registers a certain number of entities to watch. We need to determine how screen space is allocated, so we want to do that in one pass. We first sort the entities and then start working through them--creating an initial state of the viewport and clip plane and iterating forward.


  struct ClipCamera {
    SDL_Rect clip, viewport;
  };

  // The number of clippings == number of rendering passes
  std::vector clippings;

  // sort entities by position
  entities_followed.sort();

  int32_t min_rect_w = (GAME_WIDTH / num_entities);
  int32_t min_rect_hw = min_rect_w / 2;
  int32_t current_entity_index = 0;
  int32_t last_left = 0;

  while (current_entity_index < num_entities) {
    int64_t left, right, top, bottom;
    ClipCamera clip_cam;

Setup the initial criteria for our current viewport

The first entity in the loop is our initial box that we will try to stuff other entities nearby into.


    // Setup the initial camera where we are going to try and merge players together
    {
      const auto& entity = entities_followed[current_entity_index];
      auto& pos = entity->position();
      auto bbox = entity->bbox()[0];

      int64_t x = (pos.x + bbox.w / 2.0);
      left = x - min_rect_hw;
      right = x + min_rect_hw;
      top = 0;
      bottom = GAME_HEIGHT;
    }

Iterate through the remaining entities and merge

From this point forward, we go through the sorted list and try to add entities to the rectangle


    int32_t t_right = right;
    while (++current_entity_index < num_entities) {
      const auto& entity = entities_followed[current_entity_index];
      auto& pos = entity->position();
      auto bbox = entity->bbox()[0];
      int64_t x = (pos.x + bbox.w / 2.0);
      t_right += min_rect_hw;
      if (x <= t_right) {
        right += min_rect_w;
        t_right = right;
      } else {
        break;
      }
    }

Add the camera as a pass for rendering

Once we break out of the loop, we have found an entity that doesn't fit in the rectangle, or we have went through all entities. Now we can add the camera to our list of cameras that will be rendered.


    clip_cam.clip.x = left;
    clip_cam.clip.y = top;
    clip_cam.clip.w = (right - left);
    clip_cam.clip.h = GAME_HEIGHT;

    clip_cam.viewport.x = last_left;
    clip_cam.viewport.y = 0;
    clip_cam.viewport.w = clip_cam.clip.w - 1;
    clip_cam.viewport.h = clip_cam.clip.h;

    last_left += clip_cam.clip.w;

    clippings.push_back(clip_cam);
  }

Render the cameras

The next step is to iterate through our new cameras and set the viewport for rendering. The entities that should be rendered are off-set by the clip amount to keep things centered. Ideally, we would use a scenegraph to keep this efficient. That's not implemented yet though. So, our scenegraph just returns all entities :\


  for (const auto& clip_cam : clippings) {
    SDL_RenderSetViewport(sdl.renderer, &clip_cam.viewport);

    for (auto w : will_render) {
      auto transformed_dst = w.dst;

      if (!w.absolute_positioning) {
        transformed_dst.x -= clip_cam.clip.x;
        transformed_dst.y -= clip_cam.clip.y;
      }

      SDL_RenderCopyEx(sdl.renderer, w.texture.get(), &w.src, &transformed_dst,
        w.angle, nullptr, static_cast(w.flip_mask()));

    }

    ++index;
  }

And there we have it. A fun and fluid split screen.