Skip to content
GameGenresEspañol
Action

Open-world survival craft

You start with nothing in an open world and have to gather resources, craft tools and build a shelter to stay alive against hunger, weather and creatures.

Also known as: survival sandbox, survival crafting, open world survival craft

Defining mechanics

  • Gathering resources from the world (wood, stone, food), by hand at first and then with tools.
  • Crafting recipes that turn materials into tools, weapons and buildings; each new tool speeds up gathering or opens resources you could not take before.
  • Survival meters (hunger, thirst, temperature, health) that drop over time and have to be refilled.
  • A day and night cycle in which the day is for gathering and the night is more dangerous.
  • Building a shelter or a base that protects you and stores what you have gathered.

Gameplay loop

Go out to gather, come back with materials, craft something better and use it to go further or get new resources, while keeping an eye on your meters.

Our demo

Controls. Arrows or WASD to move; hold space or E next to a resource to gather it. 1 crafts the axe and 2 the shelter, or click the recipe buttons. On mobile, the on-screen buttons and tapping the recipes.

Open full screen

An island seen from above with trees, rocks and berry bushes. You gather wood and stone, craft an axe to chop faster and, with enough material, build a shelter. Meanwhile your food drops, faster at night.

Made with Kaplay, in about 161 lines of JavaScript.

How it's made

  1. Setup and controls

    The game is created and virtual buttons are defined for moving, gathering and crafting. The constants set the pace of the game, such as how fast food drops, how much a berry is worth and how long day and night last.

    const k = kaplay({
      width: 640,
      height: 360,
      letterbox: true, pixelDensity,
      background: [96, 150, 84],
      buttons: {
        left: { keyboard: ["left", "a"] }, right: { keyboard: ["right", "d"] },
        up: { keyboard: ["up", "w"] }, down: { keyboard: ["down", "s"] },
        use: { keyboard: ["space", "e"] },
        axe: { keyboard: ["1"] },
        shelter: { keyboard: ["2"] },
      },
    });
    touchControls(k, [{ button: "left", label: "◀" }, { button: "up", label: "▲" }, { button: "down", label: "▼" },
      { button: "right", label: "▶" }, { button: "use", label: "✋", side: "right" }]);
    
    const SPEED = 120;
    const REACH = 34; // how close you must be to gather something
    const HUNGER_DRAIN = 3; // food points lost per second during the day
    const NIGHT_COST = 1.8; // nights are cold: you burn food this many times faster
    const BERRY_FOOD = 25;
    const DAY_LENGTH = 25; // seconds of daylight…
    const NIGHT_LENGTH = 15; // …and of night
  2. The world and its resources

    Each kind of resource (tree, rock, bush) is described in a table with what it gives, how many units it holds and how long one unit takes by hand. Adding a new resource means adding a row. Positions are fixed so that every game starts the same.

    // Fixed positions, so every game starts the same and is easy to compare in class.
    const TREES = [[90, 90], [150, 250], [230, 120], [300, 270], [380, 85], [455, 205], [525, 105], [585, 270], [120, 175], [545, 185]];
    const ROCKS = [[205, 205], [340, 160], [480, 285], [600, 80], [60, 280]];
    const BUSHES = [[265, 70], [420, 290], [610, 175]];
    // Each kind of resource: what it gives, how much it holds and how long one unit takes by hand.
    const KINDS = {
      tree: { item: "wood", amount: 3, time: 1.6, shape: () => circle(15), color: [34, 100, 48] },
      rock: { item: "stone", amount: 3, time: 1.4, shape: () => polygon([vec2(-14, 6), vec2(-8, -10), vec2(8, -12), vec2(15, 4), vec2(4, 12)]), color: [150, 150, 160] },
      bush: { item: "food", amount: 2, time: 0.4, shape: () => circle(11), color: [50, 120, 60] },
    };
    function spawn(kind, [x, y]) {
      const def = KINDS[kind];
      return add([def.shape(), color(...def.color), pos(x, y), anchor("center"), area(), scale(1), z(2),
        { kind, amount: def.amount, progress: 0, regrow: 0 }, "resource"]);
    }
  3. The player and the inventory

    The inventory is stored on the player object itself, with food, wood, stone and whether you have an axe. The player moves in eight directions and cannot leave the island.

    // The inventory lives on the player: food (the hunger meter), wood, stone and tools.
    const player = add([circle(9), color(250, 214, 140), outline(2, rgb(90, 60, 30)), pos(320, 190), anchor("center"),
      z(5), { food: 100, wood: 0, stone: 0, axe: false }, "player"]);
    player.onUpdate(() => {
      const axis = (a, b) => (isButtonDown(b) ? 1 : 0) - (isButtonDown(a) ? 1 : 0);
      const dir = vec2(axis("left", "right"), axis("up", "down"));
      if (dir.len() > 0) player.move(dir.unit().scale(SPEED));
      player.pos = vec2(clamp(player.pos.x, 10, 630), clamp(player.pos.y, 50, 306)); // stay on the island
    });
  4. Gathering

    If you hold the button next to a resource, a bar fills up and when it is full you get one unit. If you walk away, the progress is lost. Each time you take something, the resource shrinks; trees and rocks disappear when they run out, but bushes grow berries again. The bar's time depends on your tools, and that is what crafting improves.

    // Hold "use" next to a resource: a bar fills up and you get one unit.
    // How long it takes depends on your tools, and that is what crafting improves.
    const gatherTime = (r) => (r.kind === "tree" && player.axe ? 0.45 : KINDS[r.kind].time);
    let focus = null; // the resource you are next to, if any
    player.onUpdate(() => {
      const near = get("resource").filter((r) => r.amount > 0 && r.pos.dist(player.pos) < REACH);
      near.sort((a, b) => a.pos.dist(player.pos) - b.pos.dist(player.pos));
      if (focus && focus !== near[0]) focus.progress = 0; // walking away loses the progress
      focus = near[0] ?? null;
      if (!focus || !isButtonDown("use")) {
        if (focus) focus.progress = 0;
        return;
      }
      focus.progress += dt() / gatherTime(focus);
      if (focus.progress < 1) return;
      focus.progress = 0;
      focus.amount--;
      const item = KINDS[focus.kind].item;
      if (item === "food") player.food = Math.min(100, player.food + BERRY_FOOD);
      else player[item]++;
      popup(focus.pos, item);
      if (focus.amount === 0 && focus.kind !== "bush") destroy(focus); // bushes grow back, trees do not
      else focus.scale = vec2(0.6 + (0.4 * focus.amount) / KINDS[focus.kind].amount);
    });
    const NAMES = { wood: { es: "madera", en: "wood" }, stone: { es: "piedra", en: "stone" }, food: { es: "comida", en: "food" } };
    function popup(p, item) {
      const label = add([text("+1 " + t(NAMES[item]), { size: 13, font: FONT }), pos(p.sub(0, 20)), anchor("center"), opacity(1), z(9)]);
      label.onUpdate(() => {
        label.pos.y -= 30 * dt();
        label.opacity -= dt();
        if (label.opacity <= 0) destroy(label);
      });
    }
  5. Crafting

    Each recipe says how much wood and stone it needs. Crafting subtracts the materials and changes something in the game. The axe makes chopping more than three times faster, and the shelter wins you the game. The buttons light up when you have enough material, so the player can see the next goal.

    // A recipe turns materials into something that changes how you play.
    const RECIPES = {
      axe: { wood: 3, stone: 2, name: { es: "Hacha", en: "Axe" } },
      shelter: { wood: 15, stone: 9, name: { es: "Refugio", en: "Shelter" } },
    };
    const canCraft = (id) => player.wood >= RECIPES[id].wood && player.stone >= RECIPES[id].stone && !(id === "axe" && player.axe);
    function craft(id) {
      if (id === "axe" && player.axe) return say(t({ es: "Ya tienes un hacha.", en: "You already have an axe." }));
      if (!canCraft(id)) return say(t({ es: "Te faltan materiales.", en: "Not enough materials." }));
      player.wood -= RECIPES[id].wood;
      player.stone -= RECIPES[id].stone;
      if (id === "shelter") return go("end", true, day());
      player.axe = true;
      say(t({ es: "¡Hacha! Ahora talas mucho más rápido.", en: "Axe! Now you chop much faster." }));
    }
    onButtonPress("axe", () => craft("axe"));
    onButtonPress("shelter", () => craft("shelter"));
    // On-screen recipe buttons: click or tap them (or press 1 and 2).
    Object.keys(RECIPES).forEach((id, i) => {
      const r = RECIPES[id];
      const button = add([rect(250, 30, { radius: 6 }), pos(70 + i * 270, 322), area(), color(60, 70, 60), z(11)]);
      button.add([text(`(${i + 1}) ${t(r.name)}: ${r.wood} ${t(NAMES.wood)} + ${r.stone} ${t(NAMES.stone)}`, { size: 13, font: FONT }), pos(125, 15), anchor("center")]);
      button.onClick(() => craft(id));
      button.onUpdate(() => (button.color = canCraft(id) ? rgb(70, 140, 70) : rgb(60, 70, 60)));
    });
  6. Hunger, day and night

    Food drops all the time, and if it reaches zero you lose. At night it drops almost twice as fast, because of the cold. A clock counts the seconds, and the day number and whether it is night come from it. Night is a dark layer whose opacity slowly approaches the right value, so the change is gradual. This pressure forces you to choose between eating and gathering more.

    // Food drops all the time, faster at night; at zero you lose.
    // Bushes grow a new berry every few seconds, so food is renewable but slow.
    let clock = 0;
    const day = () => Math.floor(clock / (DAY_LENGTH + NIGHT_LENGTH)) + 1;
    const isNight = () => clock % (DAY_LENGTH + NIGHT_LENGTH) > DAY_LENGTH;
    onUpdate(() => {
      clock += dt();
      messageTime -= dt();
      player.food -= HUNGER_DRAIN * (isNight() ? NIGHT_COST : 1) * dt();
      if (player.food <= 0) go("end", false, day());
      for (const b of get("resource").filter((r) => r.kind === "bush" && r.amount < 3))
        if ((b.regrow += dt()) > 8) (b.regrow = 0), b.amount++, (b.scale = vec2(0.6 + (0.4 * b.amount) / 3));
    });
    // Night is a dark layer whose opacity follows the clock.
    const night = add([rect(width(), height()), color(10, 14, 40), opacity(0), z(8)]);
    night.onUpdate(() => (night.opacity = lerp(night.opacity, isNight() ? 0.55 : 0, dt() * 2)));
  7. The HUD

    The food bar turns red when it is running low. The text shows the inventory and the day. Berries are drawn on the bushes and the gathering bar over the resource you are working on, because important information should be where you are looking.

    add([z(10), { draw() {
      // Berries on the bushes and the gathering bar over the resource you are working on.
      for (const b of get("resource").filter((r) => r.kind === "bush"))
        for (let i = 0; i < b.amount; i++) drawCircle({ pos: b.pos.add(-6 + i * 6, -2 + (i % 2) * 5), radius: 3, color: rgb(220, 40, 60) });
      if (focus) {
        drawRect({ pos: focus.pos.add(-16, -26), width: 32, height: 5, color: rgb(30, 30, 30) });
        drawRect({ pos: focus.pos.add(-16, -26), width: 32 * focus.progress, height: 5, color: rgb(255, 230, 120) });
      }
      drawRect({ width: width(), height: 40, color: rgb(30, 36, 30), opacity: 0.85 });
      drawText({ text: t({ es: "Comida", en: "Food" }), pos: vec2(12, 12), size: 14, font: FONT });
      drawRect({ pos: vec2(70, 12), width: 120, height: 14, color: rgb(70, 40, 40) });
      drawRect({ pos: vec2(70, 12), width: 1.2 * Math.max(0, player.food), height: 14, color: player.food < 30 ? rgb(230, 70, 60) : rgb(240, 170, 60) });
      const axe = player.axe ? t({ es: "sí", en: "yes" }) : t({ es: "no", en: "no" });
      const when = isNight() ? t({ es: " · noche", en: " · night" }) : "";
      drawText({ text: `${t({ es: "Madera", en: "Wood" })} ${player.wood}   ${t({ es: "Piedra", en: "Stone" })} ${player.stone}   ${t({ es: "Hacha", en: "Axe" })}: ${axe}   ${t({ es: "Día", en: "Day" })} ${day()}${when}`, pos: vec2(205, 12), size: 14, font: FONT });
      if (messageTime > 0) drawText({ text: message, pos: vec2(width() / 2, 298), size: 14, font: FONT, anchor: "center", color: rgb(255, 245, 200) });
    } }]);
  8. Scenes

    The game has three scenes, title, game and end, and go() jumps between them. The end screen shows the day you finished on, so you can compare runs.

    function screen(title, subtitle, next) {
      add([text(title, { size: 34, font: FONT, align: "center" }), pos(center().sub(0, 50)), anchor("center")]);
      add([text(subtitle, { size: 15, font: FONT, align: "center", width: 560 }), pos(center().add(0, 30)), anchor("center"), color(235, 245, 225)]);
      onKeyPress(["enter"], next);
      onButtonPress("use", next);
      onClick(next);
    }
    
    scene("title", () =>
      screen(
        t({ es: "Supervivencia en mundo abierto", en: "Open-world survival craft" }),
        t({
          es: "Flechas o WASD para moverte. Mantén espacio (o E) junto a un árbol, una roca o un arbusto para recolectar.\nFabrica un hacha (1) para talar más rápido y un refugio (2) para ganar.\nNo dejes que la comida llegue a cero: de noche hace frío y gastas más.\nPulsa espacio o toca para empezar.",
          en: "Arrows or WASD to move. Hold space (or E) next to a tree, a rock or a bush to gather.\nCraft an axe (1) to chop faster and a shelter (2) to win.\nDo not let your food reach zero: nights are cold and you burn more.\nPress space or tap to start.",
        }),
        () => go("game"),
      ),
    );
    
    scene("end", (won, days) =>
      screen(
        won ? t({ es: "Refugio construido", en: "Shelter built" }) : t({ es: "Te venció el hambre", en: "Hunger got you" }),
        t({ es: `Día ${days}. Pulsa espacio o toca para jugar de nuevo.`, en: `Day ${days}. Press space or tap to play again.` }),
        () => go("game"),
      ),
    );
    
    go("title");

What we simplified

  • The island fits on one screen and is always the same; in the real genre the world is large and usually randomly generated.
  • There is a single meter (food) and two recipes; there is no thirst, temperature or creatures that attack you.
  • Night brings no new dangers, it only gets darker and makes you burn food faster because of the cold.

To modify it

  • Add a campfire (wood + stone) that slows food loss at night if you stand near it.
  • Make chopped trees leave a sapling that grows back, and think about how it changes the strategy.
  • Add a pickaxe that is required to mine stone from big rocks.
Full code
// Survival craft demo
// Engine: Kaplay 3001 (https://kaplayjs.com). MIT licence.
import kaplay from "../../vendor/kaplay.mjs";
import { t, FONT, touchControls, pixelDensity } from "../common.js";

// #region setup
const k = kaplay({
  width: 640,
  height: 360,
  letterbox: true, pixelDensity,
  background: [96, 150, 84],
  buttons: {
    left: { keyboard: ["left", "a"] }, right: { keyboard: ["right", "d"] },
    up: { keyboard: ["up", "w"] }, down: { keyboard: ["down", "s"] },
    use: { keyboard: ["space", "e"] },
    axe: { keyboard: ["1"] },
    shelter: { keyboard: ["2"] },
  },
});
touchControls(k, [{ button: "left", label: "◀" }, { button: "up", label: "▲" }, { button: "down", label: "▼" },
  { button: "right", label: "▶" }, { button: "use", label: "✋", side: "right" }]);

const SPEED = 120;
const REACH = 34; // how close you must be to gather something
const HUNGER_DRAIN = 3; // food points lost per second during the day
const NIGHT_COST = 1.8; // nights are cold: you burn food this many times faster
const BERRY_FOOD = 25;
const DAY_LENGTH = 25; // seconds of daylight…
const NIGHT_LENGTH = 15; // …and of night
// #endregion

// #region world
// Fixed positions, so every game starts the same and is easy to compare in class.
const TREES = [[90, 90], [150, 250], [230, 120], [300, 270], [380, 85], [455, 205], [525, 105], [585, 270], [120, 175], [545, 185]];
const ROCKS = [[205, 205], [340, 160], [480, 285], [600, 80], [60, 280]];
const BUSHES = [[265, 70], [420, 290], [610, 175]];
// Each kind of resource: what it gives, how much it holds and how long one unit takes by hand.
const KINDS = {
  tree: { item: "wood", amount: 3, time: 1.6, shape: () => circle(15), color: [34, 100, 48] },
  rock: { item: "stone", amount: 3, time: 1.4, shape: () => polygon([vec2(-14, 6), vec2(-8, -10), vec2(8, -12), vec2(15, 4), vec2(4, 12)]), color: [150, 150, 160] },
  bush: { item: "food", amount: 2, time: 0.4, shape: () => circle(11), color: [50, 120, 60] },
};
function spawn(kind, [x, y]) {
  const def = KINDS[kind];
  return add([def.shape(), color(...def.color), pos(x, y), anchor("center"), area(), scale(1), z(2),
    { kind, amount: def.amount, progress: 0, regrow: 0 }, "resource"]);
}
// #endregion

scene("game", () => {
  for (const p of TREES) spawn("tree", p);
  for (const p of ROCKS) spawn("rock", p);
  for (const p of BUSHES) spawn("bush", p);
  let message = "";
  let messageTime = 0;
  const say = (m) => ((message = m), (messageTime = 2.5));

  // #region player
  // The inventory lives on the player: food (the hunger meter), wood, stone and tools.
  const player = add([circle(9), color(250, 214, 140), outline(2, rgb(90, 60, 30)), pos(320, 190), anchor("center"),
    z(5), { food: 100, wood: 0, stone: 0, axe: false }, "player"]);
  player.onUpdate(() => {
    const axis = (a, b) => (isButtonDown(b) ? 1 : 0) - (isButtonDown(a) ? 1 : 0);
    const dir = vec2(axis("left", "right"), axis("up", "down"));
    if (dir.len() > 0) player.move(dir.unit().scale(SPEED));
    player.pos = vec2(clamp(player.pos.x, 10, 630), clamp(player.pos.y, 50, 306)); // stay on the island
  });
  // #endregion

  // #region gathering
  // Hold "use" next to a resource: a bar fills up and you get one unit.
  // How long it takes depends on your tools, and that is what crafting improves.
  const gatherTime = (r) => (r.kind === "tree" && player.axe ? 0.45 : KINDS[r.kind].time);
  let focus = null; // the resource you are next to, if any
  player.onUpdate(() => {
    const near = get("resource").filter((r) => r.amount > 0 && r.pos.dist(player.pos) < REACH);
    near.sort((a, b) => a.pos.dist(player.pos) - b.pos.dist(player.pos));
    if (focus && focus !== near[0]) focus.progress = 0; // walking away loses the progress
    focus = near[0] ?? null;
    if (!focus || !isButtonDown("use")) {
      if (focus) focus.progress = 0;
      return;
    }
    focus.progress += dt() / gatherTime(focus);
    if (focus.progress < 1) return;
    focus.progress = 0;
    focus.amount--;
    const item = KINDS[focus.kind].item;
    if (item === "food") player.food = Math.min(100, player.food + BERRY_FOOD);
    else player[item]++;
    popup(focus.pos, item);
    if (focus.amount === 0 && focus.kind !== "bush") destroy(focus); // bushes grow back, trees do not
    else focus.scale = vec2(0.6 + (0.4 * focus.amount) / KINDS[focus.kind].amount);
  });
  const NAMES = { wood: { es: "madera", en: "wood" }, stone: { es: "piedra", en: "stone" }, food: { es: "comida", en: "food" } };
  function popup(p, item) {
    const label = add([text("+1 " + t(NAMES[item]), { size: 13, font: FONT }), pos(p.sub(0, 20)), anchor("center"), opacity(1), z(9)]);
    label.onUpdate(() => {
      label.pos.y -= 30 * dt();
      label.opacity -= dt();
      if (label.opacity <= 0) destroy(label);
    });
  }
  // #endregion

  // #region crafting
  // A recipe turns materials into something that changes how you play.
  const RECIPES = {
    axe: { wood: 3, stone: 2, name: { es: "Hacha", en: "Axe" } },
    shelter: { wood: 15, stone: 9, name: { es: "Refugio", en: "Shelter" } },
  };
  const canCraft = (id) => player.wood >= RECIPES[id].wood && player.stone >= RECIPES[id].stone && !(id === "axe" && player.axe);
  function craft(id) {
    if (id === "axe" && player.axe) return say(t({ es: "Ya tienes un hacha.", en: "You already have an axe." }));
    if (!canCraft(id)) return say(t({ es: "Te faltan materiales.", en: "Not enough materials." }));
    player.wood -= RECIPES[id].wood;
    player.stone -= RECIPES[id].stone;
    if (id === "shelter") return go("end", true, day());
    player.axe = true;
    say(t({ es: "¡Hacha! Ahora talas mucho más rápido.", en: "Axe! Now you chop much faster." }));
  }
  onButtonPress("axe", () => craft("axe"));
  onButtonPress("shelter", () => craft("shelter"));
  // On-screen recipe buttons: click or tap them (or press 1 and 2).
  Object.keys(RECIPES).forEach((id, i) => {
    const r = RECIPES[id];
    const button = add([rect(250, 30, { radius: 6 }), pos(70 + i * 270, 322), area(), color(60, 70, 60), z(11)]);
    button.add([text(`(${i + 1}) ${t(r.name)}: ${r.wood} ${t(NAMES.wood)} + ${r.stone} ${t(NAMES.stone)}`, { size: 13, font: FONT }), pos(125, 15), anchor("center")]);
    button.onClick(() => craft(id));
    button.onUpdate(() => (button.color = canCraft(id) ? rgb(70, 140, 70) : rgb(60, 70, 60)));
  });
  // #endregion

  // #region survival
  // Food drops all the time, faster at night; at zero you lose.
  // Bushes grow a new berry every few seconds, so food is renewable but slow.
  let clock = 0;
  const day = () => Math.floor(clock / (DAY_LENGTH + NIGHT_LENGTH)) + 1;
  const isNight = () => clock % (DAY_LENGTH + NIGHT_LENGTH) > DAY_LENGTH;
  onUpdate(() => {
    clock += dt();
    messageTime -= dt();
    player.food -= HUNGER_DRAIN * (isNight() ? NIGHT_COST : 1) * dt();
    if (player.food <= 0) go("end", false, day());
    for (const b of get("resource").filter((r) => r.kind === "bush" && r.amount < 3))
      if ((b.regrow += dt()) > 8) (b.regrow = 0), b.amount++, (b.scale = vec2(0.6 + (0.4 * b.amount) / 3));
  });
  // Night is a dark layer whose opacity follows the clock.
  const night = add([rect(width(), height()), color(10, 14, 40), opacity(0), z(8)]);
  night.onUpdate(() => (night.opacity = lerp(night.opacity, isNight() ? 0.55 : 0, dt() * 2)));
  // #endregion

  // #region hud
  add([z(10), { draw() {
    // Berries on the bushes and the gathering bar over the resource you are working on.
    for (const b of get("resource").filter((r) => r.kind === "bush"))
      for (let i = 0; i < b.amount; i++) drawCircle({ pos: b.pos.add(-6 + i * 6, -2 + (i % 2) * 5), radius: 3, color: rgb(220, 40, 60) });
    if (focus) {
      drawRect({ pos: focus.pos.add(-16, -26), width: 32, height: 5, color: rgb(30, 30, 30) });
      drawRect({ pos: focus.pos.add(-16, -26), width: 32 * focus.progress, height: 5, color: rgb(255, 230, 120) });
    }
    drawRect({ width: width(), height: 40, color: rgb(30, 36, 30), opacity: 0.85 });
    drawText({ text: t({ es: "Comida", en: "Food" }), pos: vec2(12, 12), size: 14, font: FONT });
    drawRect({ pos: vec2(70, 12), width: 120, height: 14, color: rgb(70, 40, 40) });
    drawRect({ pos: vec2(70, 12), width: 1.2 * Math.max(0, player.food), height: 14, color: player.food < 30 ? rgb(230, 70, 60) : rgb(240, 170, 60) });
    const axe = player.axe ? t({ es: "sí", en: "yes" }) : t({ es: "no", en: "no" });
    const when = isNight() ? t({ es: " · noche", en: " · night" }) : "";
    drawText({ text: `${t({ es: "Madera", en: "Wood" })} ${player.wood}   ${t({ es: "Piedra", en: "Stone" })} ${player.stone}   ${t({ es: "Hacha", en: "Axe" })}: ${axe}   ${t({ es: "Día", en: "Day" })} ${day()}${when}`, pos: vec2(205, 12), size: 14, font: FONT });
    if (messageTime > 0) drawText({ text: message, pos: vec2(width() / 2, 298), size: 14, font: FONT, anchor: "center", color: rgb(255, 245, 200) });
  } }]);
  // #endregion
});

// #region scenes
function screen(title, subtitle, next) {
  add([text(title, { size: 34, font: FONT, align: "center" }), pos(center().sub(0, 50)), anchor("center")]);
  add([text(subtitle, { size: 15, font: FONT, align: "center", width: 560 }), pos(center().add(0, 30)), anchor("center"), color(235, 245, 225)]);
  onKeyPress(["enter"], next);
  onButtonPress("use", next);
  onClick(next);
}

scene("title", () =>
  screen(
    t({ es: "Supervivencia en mundo abierto", en: "Open-world survival craft" }),
    t({
      es: "Flechas o WASD para moverte. Mantén espacio (o E) junto a un árbol, una roca o un arbusto para recolectar.\nFabrica un hacha (1) para talar más rápido y un refugio (2) para ganar.\nNo dejes que la comida llegue a cero: de noche hace frío y gastas más.\nPulsa espacio o toca para empezar.",
      en: "Arrows or WASD to move. Hold space (or E) next to a tree, a rock or a bush to gather.\nCraft an axe (1) to chop faster and a shelter (2) to win.\nDo not let your food reach zero: nights are cold and you burn more.\nPress space or tap to start.",
    }),
    () => go("game"),
  ),
);

scene("end", (won, days) =>
  screen(
    won ? t({ es: "Refugio construido", en: "Shelter built" }) : t({ es: "Te venció el hambre", en: "Hunger got you" }),
    t({ es: `Día ${days}. Pulsa espacio o toca para jugar de nuevo.`, en: `Day ${days}. Press space or tap to play again.` }),
    () => go("game"),
  ),
);

go("title");
// #endregion

Design challenges

  • Balancing how fast the meters drop against how fast you gather, so there is pressure without it becoming tedious.
  • A recipe tree that sets clear goals and in which each craft truly changes how you play.
  • Making gathering, the most repeated action, feel good through visual and sound feedback.
  • Giving direction in an open world without taking freedom away from the player.

History

  1. 2003
    OriginStranded

    Stranded (2003), a simple free game about a castaway on an island, already had a concept very close to today's survival games; Minecraft (2009), with its loop of gathering and crafting by day and defending yourself by night, popularised the formula.

    Survival game (Wikipedia)
  2. 2003
    Stranded

    In Unreal Software's Stranded, a castaway manages hunger, thirst and sleep, picks up branches, stones and vines, crafts tools and shelters, and builds a raft to escape.

    Stranded (video game) (Wikipedia)
  3. 2009
    Minecraft

    The first public version of Minecraft comes out; in its survival mode you gather wood and stone to craft, eat so as not to starve and need shelter because monsters come out at night.

    Minecraft (Wikipedia)
  4. 2013
    Don't Starve

    Klei Entertainment's Don't Starve adds a sanity meter to hunger and health, nights in which darkness kills, and permanent death in a randomly generated world.

    Don't Starve (Wikipedia)
  5. 2013

    Facepunch's Rust enters early access: multiplayer only, you start with a rock and a torch, and the main threat is other players, who can raid your base.

    Rust (video game) (Wikipedia)
  6. 2016
    P.Craft

    NuSan releases P.Craft for the PICO-8 fantasy console: an island, trees to chop, a workbench and a boat to escape, all playable in the browser.

    P.Craft (Lexaloffle BBS, PICO-8)
  7. 2018
    Subnautica

    Subnautica takes the genre underwater, with oxygen, hunger and thirst to watch, and its developers choose not to include lethal weapons.

    Subnautica (Wikipedia)
  8. 2021
    Valheim

    Valheim, from the Swedish studio Iron Gate, sells five million copies in its first month of early access, with up to ten players building and surviving together.

    Valheim (Wikipedia)

Reference games

2003

Stranded

Unreal Software (Peter Schauß). Windows

You are a castaway on a desert island. You manage hunger, thirst and sleep, pick up branches, stones and vines, craft tools and shelters, and build a raft to escape. Its developers released it for free.

2009

Minecraft

Mojang Studios (Markus Persson). Windows, macOS, Linux, Android, iOS

A randomly generated world of blocks. In survival mode you gather wood and stone to craft tools, eat so as not to starve and take shelter at night, when monsters come out; in creative mode you just build.

2013

Don't Starve

Klei Entertainment. Windows, macOS, Linux

You survive in a randomly generated world by watching three meters, hunger, health and sanity, while you gather and craft from recipes. At night the darkness kills you if you have no light, and death is permanent.

2018

Subnautica

Unknown Worlds Entertainment. Windows, macOS, PlayStation 4, Xbox One

After crashing on an ocean planet, you explore the seabed while watching your oxygen, hunger and thirst, collect resources and blueprints, build bases and submarines, and look for a way to escape. It has no lethal weapons.

2021

Valheim

Iron Gate Studio. Windows, Linux

Norse-inspired castaways in a world generated region by region. You gather materials, craft tools and gear, put up shelters and defeat bosses, alone or with up to ten people.

Exercise

In the demo, change the cost of the axe and the shelter and how fast food drops, and time how long it takes to win with and without the axe. Then add a new recipe (for example, a campfire that slows food loss at night) and discuss in which order it pays to craft things.

Sources