Skip to content

Creating a Project

Brandon Davis edited this page Nov 1, 2022 · 8 revisions

In this guide I will be walking you through creating a new project using the Terminal Velocity Engine (TVE)

Setup Dependencies

The first thing you should do is create a new gradle java project.

Next using git add https://github.com/TerminalVelocityCabbage/TerminalVelocityEngine as a git sub-module.

You can do this with your git client or just create a .gitmodules file with the following contents

[submodule "TerminalVelocityEngine"]
	path = TerminalVelocityEngine
	url = https://github.com/TerminalVelocityCabbage/TerminalVelocityEngine.git

Then in your build.gradle add this project to your dependencies.

dependencies {
    implementation project('TerminalVelocityEngine')
    implementation project('StudioJar')
}

Then in your settings.gradle add this.

include("TerminalVelocityEngine")
include("StudioJar")
project(':StudioJar').projectDir = file('TerminalVelocityEngine/StudioJar')

We do not handle your LWJGL dependencies for you either, so you will need to add those. Below is a full build.gradle that your project may use:

plugins {
    id 'java'
}

group 'com.terminalvelocitycabbage'
version '1.0-SNAPSHOT'
project.ext.lwjglVersion = "3.2.3"
project.ext.jomlVersion = "1.9.22"

switch (org.gradle.internal.os.OperatingSystem.current()) {
    case org.gradle.internal.os.OperatingSystem.LINUX:
        def osArch = System.getProperty("os.arch")
        project.ext.lwjglNatives = osArch.startsWith("arm") || osArch.startsWith("aarch64")
                ? "natives-linux-${osArch.contains("64") || osArch.startsWith("armv8") ? "arm64" : "arm32"}"
                : "natives-linux"
        break
    case org.gradle.internal.os.OperatingSystem.MAC_OS:
        project.ext.lwjglNatives = "natives-macos"
        break
    case org.gradle.internal.os.OperatingSystem.WINDOWS:
        project.ext.lwjglNatives = System.getProperty("os.arch").contains("64") ? "natives-windows" : "natives-windows-x86"
        break
}

repositories {
    mavenCentral()
}

dependencies {
    implementation project('TerminalVelocityEngine')
    implementation project('StudioJar')

    implementation platform("org.lwjgl:lwjgl-bom:$lwjglVersion")

    implementation "org.lwjgl:lwjgl"
    implementation "org.lwjgl:lwjgl-assimp"
    implementation "org.lwjgl:lwjgl-glfw"
    implementation "org.lwjgl:lwjgl-openal"
    implementation "org.lwjgl:lwjgl-opencl"
    implementation "org.lwjgl:lwjgl-opengl"
    implementation "org.lwjgl:lwjgl-shaderc"
    implementation "org.lwjgl:lwjgl-stb"
    runtimeOnly "org.lwjgl:lwjgl::$lwjglNatives"
    runtimeOnly "org.lwjgl:lwjgl-assimp::$lwjglNatives"
    runtimeOnly "org.lwjgl:lwjgl-glfw::$lwjglNatives"
    runtimeOnly "org.lwjgl:lwjgl-openal::$lwjglNatives"
    runtimeOnly "org.lwjgl:lwjgl-opengl::$lwjglNatives"
    runtimeOnly "org.lwjgl:lwjgl-shaderc::$lwjglNatives"
    runtimeOnly "org.lwjgl:lwjgl-stb::$lwjglNatives"
    implementation "org.joml:joml:${jomlVersion}"

    implementation 'com.github.jhg023:SimpleNet:1.6.4'
}

Once you initialize the submodule you may need to reload gradle dependencies.

Setting Up Code Structure

The next things you will need to create to have a functional project is a client and a renderer.

First create a renderer class and extend Renderer

import com.terminalvelocitycabbage.engine.client.renderer.Renderer;

public class GameRenderer extends Renderer {

    public GameRenderer(int width, int height, String title, float tickRate, boolean debugMode) {
        super(width, height, title, tickRate, debugMode);
    }
    
}

Next in your main class extend the ClientBase class and define an ID for your project for use all over your project. You should have something that looks like this:

public class GameClient extends ClientBase {

    public static final String ID = "example";

    public GameClient() {
        super(new Logger(ID), new GameRenderer(1900, 1000, "Game Client", 60, false));
    }

    public static void main(String[] args) {
        new GameClient();
    }
}

You'll notice that nothing much happens if you run this yet. We need to create a window, luckily we make this very easy to do.

Creating a window and managing scenes

First you need to assign the client context to the class you just created and init and start the game, we will just do that in the constructor of the Client class.

You can see that the renderer we are passing in a width, height, windowTitle, tickrate, and debugMode parameter. these are generally self explanatory, except tickRate just controls the number of updates per second that the game requests updates for.

    public GameClient() {
        super(new Logger(ID), new GameRenderer(1900, 1000, "Game Client", 60, false));

        //assign client context to this class
        instance = this;
        init();
        start();
    }

For TVE to know what to draw we must first setup a scene. A scene is just an object extending the Scene class provided by TVE that contains all the objects that you may draw to the screen and also houses update logic.

Let's create a scene. First create a scene class and extend Scene. a class extending Scene requires you to implement 2 methods and pass a Camera and a InputHandler to the scene. We will just create placeholders for those for now, we will get into the details of those objects later for now just copy this into your class.

public class GameScene extends Scene {

    public GameScene() {
        super(new FirstPersonCamera(60, 0.1f, 6000f), new InputHandler() {
            @Override
            public void processInput(KeyBind keyBind) {

            }
        });
    }

    @Override
    public void tick(float deltaTime) {

    }

    @Override
    public void destroy() {

    }
}

Next it is just a matter of registering this scene in the renderer and activating it. First override the init method in renderer and add the following to it:

    @Override
    public void init() {
        super.init();
        getRenderer.setVysnc(true);
        
        //Register scene
        getSceneHandler().addScene("game_scene", new GameScene());
        
        //Load the scene
        getSceneHandler().loadScene("game_scene");
    }

As you can see above, managing scenes in TVE is very easy, you just need to create a scene object and to activate it you tell the scene handler to load it, they will be initialized and run automatically.

We're almost done, just need to tell everything to turn on when the app starts. In the renderer we need to override the loop method and tell it to push a frame:

    @Override
    public void loop() {
        super.loop();
        push();
    }

Then in our client we need to init the renderer and run the renderer:

    @Override
    public void init() {
        super.init();
        getRenderer().init();
    }

    @Override
    public void start() {
        super.start();
        getRenderer().run();
    }

Running this code should open a window with your requested dimensions and tickrate.

Handling Input

Next let's get rid of that placeholder InputHandler in our scene and make our own. Start by creating a new class that extends InputHandler, we will start with a simple keybind to close the window when we press escape. we first need to create fields for the keys we want to listen to, then initialize them with keys.

public class GameInputHandler extends InputHandler {

    public static KeyBind CLOSE;

    @Override
    public void init(Window window) {
        super.init(window);

        CLOSE = new KeyBind(GLFW_KEY_ESCAPE, KeyBind.ANY, GLFW_RELEASE, KeyBind.NONE);
    }
}

Then in the processInput method we will do things with that input every frame, to close the window we will use the following code:

    @Override
    public void processInput(KeyBind keyBind) {
        if (CLOSE.isKeyPressed()) {
            Renderer.getWindow().queueClose();
        }
    }

Now all you need to do is swap out input handlers in your scene constructor and you'll have a working keybind!

    public GameScene() {
        super(new FirstPersonCamera(60, 0.1f, 6000f), new GameInputHandler());
    }

Resources

before we can draw things to the screen we need to be able to load resources like shader files from our app for the engine to pass on to the gpu. the resources system in TVE is very simple with lots of notes taken from minecraft's system for resourcepacks using idenifiers and extended to paths. This is when that ID you created at the begining of this guide comes in handy, this is your resource identifier, so you can differentiate your resourses from tve's or possibly mods in the future of your games.

The first thing we need to do is define the resource paths somewhere, there are a variety of resources you may want to loat at any given time, so we recommend a separate class to just store a few final fields. That is what we will do in this guide. For now we only need to load shaders, so we will only have one field.

public class GameResourceHandler {

    public static final ResourceManager SHADER = new ClassLoaderResourceManager(ClassLoader.getSystemClassLoader(), "assets", "shaders");

}

This will be more useful in the future, but for now just trust us, and we will move on to drawing objects with shaders!

Drawing an Object

The next big step to making a game is actually rendering stuff, so let's do that. Note that TVE does not handle all your shaders for you, we do give a lot of extra functionality to shaders like importing other shaders into your own with a resource handler and we also provide a few shader utility files that you may find useful.

First we need to create 2 shaders, a vertex shader and a fragment shader for our renderer to use to draw to the screen, let's do that. A few things to consider when seting up your resources, your ID, your path prefix, and your resource root. The expected format is going to be as follows: /src/main/resources/prefix/id/[path] so in our case we need to create shaders in the /src/main/resources/assets/example/shaders as defined by our ResourceManager that we just defined.

in there let's create a default.vert file and a default.frag file to use for our shaders. For the sake of this guide we will use some premade shaders that have logic for materials, and can do some diffuse shading on objects. Copy paste the following code into your projects:

default.vert

#version 330
layout (location = 0) in vec3 inPosition;
layout (location = 1) in vec2 inTextureCoord;
layout (location = 2) in vec3 vertexNormal;

out vec2 vertTextureCoord;
out vec3 vertVertexNormal;
out vec3 vertVertexPosition;

uniform mat4 projectionMatrix;
uniform mat4 modelViewMatrix;
uniform mat4 normalTransformationMatrix;

void main() {
    vec4 modelViewPosition = modelViewMatrix * vec4(inPosition, 1.0);
    gl_Position = projectionMatrix * modelViewPosition;
    vertTextureCoord = inTextureCoord;
    vertVertexNormal = (normalTransformationMatrix * vec4(vertexNormal, 0.0)).xyz;
    vertVertexPosition = inPosition.xyz;
}

default.frag

#version 330 core

#include "terminalvelocitycabbage:materials.frag";
#include "terminalvelocitycabbage:lights_base.frag";
#include "terminalvelocitycabbage:directional_lights.frag";

in vec2 vertTextureCoord;
in vec3 vertVertexNormal;
in vec3 vertVertexPosition;

out vec4 fragColor;

uniform DirectionalLight directionalLight;

void main() {
    setupReflectivity(material, vertTextureCoord);
    setupColors(material, vertTextureCoord);
    //the color of the fragment multiplied by the ambient light
    vec4 color = materialAmbientColor * vec4(ambientLight, 1);
    color += calcDirectionalLight(directionalLight, vertVertexPosition, vertVertexNormal);
    fragColor = color;
}

Next we need to create a shader program and build those into sendable code to the gpu. We will do this in the init method of our renderer. Before we load the scene we need to call the following lines of code:

        //Setup Shaders
        shaderHandler
                .newProgram("default")
                .queueShader(Shader.Type.VERTEX, GameResourceHandler.SHADER, new Identifier(Client.ID, "default.vert"))
                .queueShader(Shader.Type.FRAGMENT, GameResourceHandler.SHADER, new Identifier(Client.ID, "default.frag"))
                .build();

TVE provides some utilities for making this easier when you're registering a default shader program like this, since the files and program are the same name we can replace the above code with a single method call:

        //Setup Shaders
        shaderHandler.newProgram("default").queueDefaultShaders(SHADER, GameClient.ID).build();

Now we need to set those shaders up. Firstly since we have a directional light in our shaders we need an object to supply that information. Let's create a sun in our scene:

    @Override
    public void init(Window window) {
        super.init(window);

        objectHandler.add("sun", new DirectionalLight(new Vector3f(-0.68f, 0.55f, 0.42f), new Vector4f(1, 1, 0.5f, 1), 0.3f));
    }

As you can see adding an object to the scene is very simple, we just need to give it a unique name and pass an object. In this case a "DirectionalLight" which takes in a direction, a color, and an intensity.

Now to pass all the information the shader needs to the shader we can just create a method in our rederer class to setup the shaders. A function like this will work:

    private void setupShader(Camera camera, ShaderProgram shaderProgram) {
        //Camera
        shaderProgram.setUniform("projectionMatrix", camera.getProjectionMatrix());
        //Lighting
        shaderProgram.setUniform("ambientLight", 0.3f, 0.3f, 0.3f);
        shaderProgram.setUniform("specularPower", 10.0f); //Reflected light intensity
        shaderProgram.setUniform("directionalLight", sceneHandler.getActiveScene().objectHandler.getObject("sun"));
    }

Next we need a method to render the game objects that we will be adding to the scene soon. This method will need to enable the shader program that we created, setup the static unforms with the previous method, then for each object in our scene update it's transforms and render it. A super simple example is as follows:

    private void renderGameObjects(Camera camera, ShaderProgram shaderProgram) {

        shaderProgram.enable();
        setupShader(camera, shaderProgram);

        //Draw whatever changes were pushed
        for (ModeledGameObject gameObject : sceneHandler.getActiveScene().getObjectsOfType(ModeledGameObject.class)) {

            gameObject.update();

            shaderProgram.setUniform("modelViewMatrix", gameObject.getModelViewMatrix(camera.getViewMatrix()));
            shaderProgram.setUniform("normalTransformationMatrix", gameObject.getTransformationMatrix());
            shaderProgram.setUniform("material", gameObject.getModel().getMaterial());

            gameObject.render();
        }

        shaderProgram.disable();
    }

next we just need to tell the renderer to render those things in our loop method

    @Override
    public void loop() {
        super.loop();
        renderGameObjects(getSceneHandler().getActiveScene().getCamera(), shaderHandler.getShader("default"));
        push();
    }

Now we just need to give it something to render. How about a cube? In our init method of the scene we can add the folowing:

objectHandler.add("cube", new Cube(new Vector3f(0f, 0f, -3f), new Vector4f(1, 0, 0, 1), 10f)).bind();

Note that since this game object has a model we need to call .bind() on it so that the engine can setup the data for upload. Run this and you should see a red square on your screen, we promise it's a cube, the perspective just doesn't show the sides.

Moving around the scene

Looking at a cube from the same place for hours is great and all, but think of all the possibilities for viewing pleasure if we could move around and pick any place to look at it from. Let's implement that.

TVE supplies a few default input handlers in addition to the cameras. To get started we need to tie our inputs to the camera, since we are already using the FirstPersonCamera we can just as easily use the FirstPersonInputHandler, let's change our GameInputHandler class to exrtend that first person class instead:

public class GameInputHandler extends FirstPersonInputHandler {
...
}

This doesn't quite cut it yet though, we need to pass our inputs to the camera, luckily that is just as easy as a single method call, in our renderer loop method, we can just rearrange some code and add a camera.update(...) call to it.

The other thing that we need to do though is clear the previous frame and reset it back to the way it was before. That'll be just a few gl calls, we will add those in the snippet below. In the future this will have a nice api.

    @Override
    public void loop() {
        super.loop();

        //Setup the frame for drawing
        glClearColor(0.1f, 0.1f, 0.1f, 1.0f);
        glEnable(GL_DEPTH_TEST);
        glDepthFunc(GL_LEQUAL);
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

        //This is just a good measure to take since likely in the future you will have more than one scene
        if (sceneHandler.getActiveScene() instanceof GameScene scene) {
            
            //Get the input handler and the camera from the scene
            FirstPersonInputHandler inputHandler = (FirstPersonInputHandler) scene.getInputHandler();
            FirstPersonCamera camera = ((FirstPersonCamera) scene.getCamera());

            //Update the camera with events from the input handler
            camera.update(inputHandler, getDeltaTimeInMillis());
            
            //Render the scene
            renderGameObjects(getSceneHandler().getActiveScene().getCamera(), shaderHandler.getShader("default"));
            
            //Reset the input handler
            inputHandler.resetDeltas();
        }
        push();
    }

As you can see not that much has changed, we just are now testing that we are using the GameScene, creating variables to store the camera and inputHandler and we are now calling that update method on the camera. Then at the end we just set the inputs back to 0 and we're ready for the next frame.

That's using the First person camera all done! now if you run the game you can move around with WASD and (while holding right-click) look around!