CClab Project B: Beyond the Stellaris

Project link: https://n-a-e-s.github.io/CCLab-Strix-Local/Star_Field/index

1) Process: Design and Composition:

Inspiration:

The inspiration for this project comes from an art style called NASApunk, exemplified by the video game “Starfield” developed by Bethesda Studios. At the same time, other artistic works have also inspired me during the development process, such as David Bowie’s “Space Oddity” and “Starman,” as well as the movie “The Martian.”

Interface:

When starting the design, I referred to the space exploration design in “Starfield,” hoping to enable players to explore across three dimensions: the galaxy, star systems, and planetary surfaces. Additionally, I aimed to populate this galaxy with randomly generated planets and star systems. For the early user interaction methods, I opted for the traditional WASD controls for forward, backward, left, and right movements, with the spacebar and CTRL keys controlling upward and downward movements. In terms of camera control, I initially chose the built-in `orbitControl` function. However, I soon discovered that this function causes the camera to move when rotating the view, which was not ideal for the desired user experience. Consequently, I wrote my own `manualOrbitControl()` function to rotate the view. This function utilizes `cam.pan(dx);` and `cam.tilt(dy);` to adjust the camera’s orientation, providing more precise and controlled movement.Entering a selected star system and landing on a planet are primarily accomplished using the ‘M’ key on the keyboard. This feature, along with the compass-like galaxy map, owes much to the inspiration from Paradox Interactive’s game “Stellaris.”、

Additionally, in the version updated after the presentation, I experimentally added support for Xbox controller inputs for spaceship flight controls within the star system. This version has been updated on GitHub, and now the flight within the star system supports dual input from both keyboard and mouse as well as a game controller.

Ship design: In the early versions, I did not include spaceship designs, but merely allowed the camera to fly through the boundless space. Of course, this design was quickly scrapped. I then turned to designing a bespoke spaceship for this project. By utilizing the export function of Jundroo Studio’s sandbox game SimplePlanes, I was able to design and construct the spaceship within the game. I then exported it to Blender, which allowed for further importing into the project.

2) Process: Technical

Code: Since the code is a bit too long to put inside this post, I will not paste the whole code inside. Instead, I will provide the Github link toward the project along with several parts of it.

https://github.com/N-A-E-S/CCLab-Strix-Local/tree/main/Star_Field

The web layout of this project is designed to be quite simple: it includes a cover page, a main project body, and a section for reading materials. Additionally, I created a transitional animation for the webpage, inspired by the hyperspace travel effects seen in science fiction movies like Star Wars. This animation simulates the effect of moving through space at superluminal speeds.

 

https://editor.p5js.org/N-A-E-S/sketches/zPjvSiRkH(p5js link to the animation)

Returning to the p5.js script for this project, in the `preload` function, I loaded 20 possible planet textures, sun textures, star system names, and the spaceship model. As mentioned earlier, the origin of the spaceship model has already been discussed and will not be reiterated here. The planet textures were created using another segment of p5.js code where I utilized Perlin noise to draw random 2D terrain maps. This technique allows for the generation of diverse and visually interesting planetary surfaces.

https://editor.p5js.org/N-A-E-S/sketches/2KQPUw7-v

function setup() {

  createCanvas(windowWidth, windowHeight, WEBGL);

  noStroke();

  while (syslis.length < 50) {

    let x = random(width);

    let y = random(height);

    if (dist(width / 2, height / 2, x, y) < 400 && dist(width / 2, height / 2, x, y) > 100) {

      avaliable = true;

      for (let i = 0; i < syslis.length; i++) {

        if (dist(syslis[i].x, syslis[i].y, x, y) < 30) {

          avaliable = false;

          break;

        }

      }

      if (avaliable) {

        syslis.push(new system(x, y, sysname[syslis.length]));

      }

    }

  }

  for (i in syslis) {

    for (j in syslis) {

      if (i != j) {

        if (dist(syslis[i].x, syslis[i].y, syslis[j].x, syslis[j].y) < 150 && !syslis[i].road.includes(j) && !syslis[j].road.includes(i)) {

          syslis[i].road.push(j);

          syslis[j].road.push(i);

        }

      }

    }

  }

  for (i in syslis) {

    syslis[i].solarsize = random(0.5, 1.5);

    syslis[i].planetlist = [];

    let planetnum = int(random(4, 7));

    let baseDistance = 200 + 300 * syslis[i].solarsize;

    let distanceIncrement = 250;

    for (let j = 0; j < planetnum; j++) {

      let radius = baseDistance + j * distanceIncrement + j * (10, 25);

      let angle = j * 2 * PI / planetnum + random(-PI / 4, PI / 4);

      let planetPosition = createVector(

        radius * cos(angle),

        random(-250, -150),

        radius * sin(angle)

      );

      let img = eval('img' + str(int(random(1, 21))));

      let rotateSpeed = random(0.001, 0.005);

      let size = random(0.5, 0.75) + j * 0.15;

      let np = new planet(planetPosition, size, img, rotateSpeed, 0);

      syslis[i].planetlist.push(np);

      featurenum = int(random(0, 6));

      for (let k = 0; k < featurenum; k++) {

        let feature = random(featurelib);

        np.feature.push(feature);

        let featurePosition = createVector(random(-4000, 4000), -5, random(-4000, 4000));

        np.featurePositions.push(featurePosition);

      }

    }

  }

  currentSystem = syslis[0];

  currentSystem.current = true;

  cam = createCamera();

  cam.move(200, -100, 200);

  prevMouseX = mouseX;

  prevMouseY = mouseY;

  canvas.addEventListener('mousedown', function (event) {

    event.preventDefault();

  });

  document.addEventListener('wheel', function (event) {

    handleMouseWheel(event);

  });

  window.addEventListener("gamepadconnected", function (e) {

    console.log("Gamepad connected at index %d: %s. %d buttons, %d axes.",

      e.gamepad.index, e.gamepad.id,

      e.gamepad.buttons.length, e.gamepad.axes.length);

  });

}

Now Looking at the `setup` function , this is where I create the star systems within the galaxy, randomly assigning each one a type and number of planets. I use two classes, `System` and `Planet`, to facilitate this organization. These classes will be discussed in more detail later as they play crucial roles in structuring the simulation and enhancing the interaction within the virtual galaxy.

Next, I created three `div` elements on the webpage responsible for displaying information and tutorials, thanks in large part to guidance from my professor, Moon. Following this, in the `draw` function, I added the ability to switch views, allowing users to navigate between three layers: the galaxy, the star system, and the planetary surface. This section mainly involves calling functions from the `System` and `Planet` classes, along with other functions to handle the drawing and dynamics for each level.

For the second map (star system), I integrated a section of code related to the gamepad to test control functionalities with an Xbox controller. This was primarily to assess the feasibility and responsiveness of gamepad inputs within this environment.

In the third map (planetary surface), I added a function to draw planetary terrains, which also uses Perlin noise, similar in principle to the texture drawing mentioned earlier. This function is crucial for creating realistic and engaging landscapes on the planetary surfaces that players explore.

texture(curPlanet.img);

          for (let x = -4000; x < 4000 - step; x += step) {

            for (let z = -4000; z < 4000 - step; z += step) {

              beginShape(TRIANGLES);

              let v1 = createVector(x, -curPlanet.mapheight[x + 4000][z + 4000], z);

              let v2 = createVector(x + step, -curPlanet.mapheight[x + 4000 + step][z + 4000], z);

              let v3 = createVector(x + step, -curPlanet.mapheight[x + 4000 + step][z + 4000 + step], z + step);

              let v4 = createVector(x, -curPlanet.mapheight[x + 4000][z + 4000 + step], z + step);

              vertex(v1.x, v1.y, v1.z);

              vertex(v2.x, v2.y, v2.z);

              vertex(v3.x, v3.y, v3.z);

              vertex(v1.x, v1.y, v1.z);

              vertex(v3.x, v3.y, v3.z);

              vertex(v4.x, v4.y, v4.z);

              endShape();

            }

          }

Additionally, I implemented a landing sequence within the program. This sequence ensures that the spaceship gradually descends to the planet’s surface, providing a realistic approach and touchdown experience. This feature enhances the immersion, allowing players to experience a smooth transition from space to planetary exploration.

Following that, I introduced a series of functions designed to facilitate the control of the spaceship and camera movements using the keyboard and mouse.

function handleMouseWheel(event) {

  let zoomSensitivity = 0.1;

  if (event.deltaY < 0) {

    camDistance -= zoomSensitivity * 100;

  } else {

    camDistance += zoomSensitivity * 100;

  }

  camDistance = constrain(camDistance, 100, 2000);

}

function displayPlanetInfo(info) {

  push();

  fill(255);

  textSize(16);

  textAlign(RIGHT, BOTTOM);

  text(info, width - 50, height - 50);

  pop();

}

function manualOrbitControl() {

  let dx = (pmouseX - mouseX) * 0.005;

  let dy = (pmouseY - mouseY) * -0.005;

  cam.pan(dx);

  cam.tilt(dy);

}

function keyboardCamControl() {

  let speed = 3;

  if (mouseIsPressed) {

    manualOrbitControl();

  }

  if (keys['w']) {

    cam.move(0, 0, -speed);

  }

  if (keys['s']) {

    cam.move(0, 0, speed);

  }

  if (keys['a']) {

    cam.move(-speed, 0, 0);

  }

  if (keys['d']) {

    cam.move(speed, 0, 0);

  }

  if (keys[' ']) {

    cam.move(0, -speed, 0);

  }

  if (keys['control']) {

    cam.move(0, speed, 0);

  }

}

function keyboadControl() {

  let speed = 3;

  if (keys['w']) {

    shipZ -= speed * cos(modelRotationY);

    shipX -= speed * sin(modelRotationY);

  }

  if (keys['s']) {

    shipZ += speed * cos(modelRotationY);

    shipX += speed * sin(modelRotationY);

  }

  if (keys['a']) {

    modelRotationY += 0.02;

  }

  if (keys['d']) {

    modelRotationY -= 0.02;

  }

  if (keys[' ']) {

    shipY -= speed;

  }

  if (keys['control']) {

    shipY += speed;

  }

}

function updateCam() {

  if (mouseIsPressed) {

    cameraAngleY += (mouseX - pmouseX) * 0.01;

    cameraAngleX += (mouseY - pmouseY) * 0.01;

  }

  let camX = shipX + camDistance * sin(cameraAngleY) * cos(cameraAngleX);

  let camY = shipY - camDistance * sin(cameraAngleX);

  let camZ = shipZ - camDistance * cos(cameraAngleY) * cos(cameraAngleX);

  cam.setPosition(camX, camY, camZ);

  cam.lookAt(shipX, shipY, shipZ);

}

Finally, I wrote two classes representing the star systems and planets. The rendering of planets within these classes primarily involves applying the previously mentioned textures to spherical models. Given the extensive length of the code, I won’t display it here. Please visit the GitHub page to view the full codebase.

Actually, I am very dissatisfied with the performance of this project after landing. The content on the planets is too homogeneous, with almost nothing to explore other than color variations and a few different elements. However, the webpage is already very sluggish due to the excessive content, and it can even occupy up to 2GB of memory when running, hence the plan to add more interactive content has been shelved.

(A short video to show all that functions)

3) Reflection and Future Development:

During the process of creating this project, the first thing I learned was not to set overly ambitious goals. The final product of this project can almost only be described as a skeleton compared to the objectives I set in the proposal. Additionally, many of the goals set at that time now seem almost impossible to achieve. Additionally, choosing the right tools is crucial. Out of personal interest, I chose to work on a project in a 3D environment, but the 3D capabilities of p5.js are not that… convenient. I often spent several hours trying to implement features that could be handled in a matter of minutes with a professional 3D engine like Unity. Of course, the process was still filled with interest. However, creating a work in this way makes it difficult to fully convey the main purpose of the project to users, which is obviously not the desired outcome. 

During my presentation, the guest professor Leon suggested that I should incorporate more conceptual content into the exploration of planetary surfaces, such as having players search for ancient ruins or other collectibles. This is exactly the direction I hope to take in future development.However, due to the limitations of the webpage, future development might not be entirely based on p5.js. Perhaps in the future, after optimizing the performance of this project, I will further advance the development in the direction of playability.

4) Credits & References

Cover: Starfield developed by Bethesda Studios. Steam Link: https://store.steampowered.com/agecheck/app/1716740/

Assistant on exporting the model: SimplePlane by Jundroon. Steam Link:https://store.steampowered.com/app/397340/SimplePlanes/

Special thanks to professor Moon, who is the instructor of my CClab section, and Carrot Liu, who is the fellow for this course.