volt

Writing a 3D shooter in V/pure OpenGL in an hour

This is a post about game development capabilities of a new soon-to-be-released programming language. V is a simple compiled language designed for writing safe maintainable programs with high performance. It works great for developing web services, native apps, games.

By the end of the post we will write a simple 3D game that looks like the demo to the right. Full source code is available here.

Why is V a good language for developing games? There are several reasons:

  • Very fast compilation and live code reloading

    V compiles ≈8 million lines of code per second on my desktop computer with an i5 CPU (≈2 million per CPU core). With newer 8 core CPUs, compilation speed is ≈15 million lines of code per second.

    C++ projects I tested require several hours to compile the same amount of code.

    The compilation speed is linear, so compiling 100 million lines of code will always take only about 10 seconds.

    Of course in most cases you won't be building the entire project. V has a smart incremental build system, so compiling your changes will only take a couple of milliseconds.

    And even better news: V has live code reloading, so you won't need to exit your game and recompile it it all!

  • Easy interop with C and C++

    You can reuse all your existing C/C++ code and libraries and call them from V without any performance costs.

  • C/C++ to V translator

    You can also simply translate your entire C/C++ codebase to V. An automatic translator supports the latest standards of these languages.

    Here's a post about translating DOOM and DOOM 3 to V and making compilation 120 times faster:

    https://volt.ws/doom

  • High performance.

    Performance is the same as that of C. V's stdlib was built with a focus on performance as well. For example, since strings are immutable, creating substrings never results in allocations and copying data.

    Number of allocations is minimal. There are no unnecessary escapes. Small strings are allocated on the stack.

    Serialization is built in and doesn't use reflection so it's an order of magnitude faster than most implementations in other languages.

    There is no runtime.

  • Simple language for building large maintainble programs

    Games can get pretty big, and maintaining them becomes a problem with such a complex language as C++.

    V is much, much simpler while offering the same power to the developer. Everything you can do in C++, you can do in V.

    Since the language is so simple, and there's always only one way to do things, you will be able to jump right in any part of a large code base and feel like it was you who wrote the code.

    V also has the following features to increse maintainability:

    - Strong modular system and built in testing.

    - Global state is not allowed.

    - There's no null and everything is automatically initialized to empty values. No more null reference crashes.

    - Variables are immutable by default and functions are partially pure: function arguments are always immutable, only method's receiver can be changed.

    - Thread safety and guaranteed absence of data races. You no longer have to constantly ask yourself: "Is this thread safe?" Everything is! No perfomance costs either. For example, if you are using a hash map in a concurrent function, a thread safe hash map is used automatically. Otherwise a faster single thread hash map is used.

    - Strict automatic code formatting. It goes further than gofmt and even has a set of rules for empty lines to ensure truly one coding style.

  • Simple builds and dependency management.

    Forget about makefiles with thousands of lines of instructions, compiler flags, and include files. To build your project, no matter how big, all you need to run is

    v .
  • You no longer need a separate scripting language

    There's no more need to embed Lua or Python, learn other languages, and switch between two languages when writing your game.

    V is great for scripting.

  • No GC.

    Modern garbage collectors are very powerful and optimized. However for the best performance and latency it's better to use a language without a GC.

    You won't have to manually free the memory either! V's memory management is similar to Rust, but it's much easier.

    Time to to start working on our 3D game. We won't be using any frameworks or game engines. The resulting binary is going to be about 100 KB.

    First we need to create a window with an OpenGL context. We'll be using glfw:

    module main
    
    import glfw
    import gl 
    
    fn main() {
        glfw.init()
        wnd := glfw.create_window({ 
            title: 'game' 
            width:  700
            height: 500
        }) 
        wnd.make_context_current()
        for !wnd.should_close() {
            gl.clear_color(255, 255, 255, 255)
            wnd.swap_buffers()
            glfw.wait_events()
        }
    }
    

    Let's draw something simple: a bouncing square. Drawing primitives in modern OpenGL is pretty complicated: we have to write shaders, compile them, initialize vertex buffer objects etc. Working with text is a nightmare as well.

    Luckily, V has s a small graphics library that takes care of that: gg.

    Right now gg uses OpenGL. DirectX, Metal, and Vulkan are going to be supported in the future, so you will be able to write cross platform graphical applications without worrying about implementation details.

    // Since global variables are not allowed, we have to use a  
    // Game object to store everything 
    type Game {
        gg gg.Context
    
        x  int
        y  int
        dy int
        dx int
    
        height int
        width  int
    }
    
    const (  
        W = 50  // Square's width 
    ) 
    
    #live 
    fn main() { 
        game := Game {
            dx:     1 
            dy:     1 
            height: height 
            width:  width 
            gg:     gg.new_context(width, height) 
        }
        ... 
        go game.run() 
        for !wnd.should_close() {
            gl.clear_color(255, 255, 255, 255)
            game.draw() 
            wnd.swap_buffers()
            glfw.wait_events()
        }
              
    } 
    
     
    fn (game Game) draw(){  
        game.gg.draw_rect(game.x, game.y, W, W, gx.rgb(255, 0, 255))
    } 
    
    fn (game mut Game) run() {
        for {
            // Update rectangle's position 
            game.x += game.dx
            game.y += game.dy
            // Reached an edge of the window? Bounce! 
            if game.y >= game.height - W || game.y <= 0 {
                game.dy = - game.dy
            }
            if game.x >= game.width - W || game.x <= 0 {
                game.dx = - game.dx
            }
            // Approximately 60fps. Using sleep is not reliable, 
            // but is good enough for now. 
            time.sleep(1000 / 60) 
            // This forces a redraw 
            glfw.post_empty_event()
        }
    }
    

    As you can see, by adding a #live directive, we get the changes immediately, no recompilation required.

    Let's load and draw a 3D object (mesh).

    ...

    gg has built in camera logic, moving around our 3D world is as simple as:

    ...

    Time to animate our mesh!

    ...

    Let's create a simple world and load textures.

    ...

    Finally, let's create some enemies and implement shooting:

    ... I'm sorry, this is a very early draft, it was not supposed to be published :) If you have questions or suggestions, contact me via support@volt.ws