phpgl / visu

A PHP Game Engine as a Modern OpenGL Component Framework.
https://visu.phpgl.net/
MIT License
134 stars 1 forks source link

Simple 2D game example #2

Closed ww9 closed 1 year ago

ww9 commented 1 year ago

Hi! Is there a simple 2D game example using Visu?

Or perhaps an example of how to load a png image and render on screen?

mario-deluna commented 1 year ago

Sure thing, below is a minimal example without having to bootstrap a full VISU application.

Still, the amount of code might still seem a bit ridiculous considering the target is just getting an image rendered on screen.

The thing is, the complexity gets high quickly when creating a game (as you for sure know already). And in many cases you need a lot of control over the things that make it work. VISU does not yet provide a lot of "boilerplate templates" so for a base setup there is quite a bit of things needed.

The example below should cover the basics:

I hope it will help 🙂

s

For the example create a new directory then install the required dependencies:

composer init -s dev -n
composer require phpgl/visu
composer require --dev phpgl/ide-stubs 

And the example:

example.php

<?php

use ClanCats\Container\Container;
use GL\Math\{Vec3, Vec4};
use VISU\Geo\Transform;
use VISU\Graphics\{Camera, CameraProjectionMode};
use VISU\Graphics\GLState;
use VISU\Graphics\QuadVertexArray;
use VISU\Graphics\Rendering\Pass\{BackbufferData, CallbackPass, ClearPass};
use VISU\Graphics\Rendering\{PipelineContainer, PipelineResources};
use VISU\Graphics\Rendering\{RenderPass, RenderPipeline};
use VISU\Graphics\{ShaderProgram, ShaderStage};
use VISU\Graphics\Texture;
use VISU\OS\{Input, Window, WindowHints};
use VISU\Runtime\{GameLoop, GameLoopDelegate};
use VISU\Signal\Dispatcher;

if (!defined('DS')) { define('DS', DIRECTORY_SEPARATOR); }
set_time_limit(0);

require __DIR__ . '/vendor/autoload.php';

/**
 * Simple Game class
 */
class Game implements GameLoopDelegate
{
    /**
     * The texture we want to draw later
     */
    private Texture $myImageTexture;

    /**
     * We need to create at least one shader program to draw the texture
     */
    private ShaderProgram $textureDrawShader;

    /**
     * For a 2D scene like this we need an orthographic camera
     */
    private Camera $camera;

    /**
     * Construct a new game instance
     */
    public function __construct(
        private Container $container
    ) {
        $window = $this->container->getTyped(Window::class, 'window');
        $gl = $this->container->getTyped(GLState::class, 'gl');

        // initialize the window
        $window->initailize($gl);

        // enable vsync by default
        $window->setSwapInterval(1);

        // make the input the windows event handler
        $window->setEventHandler($this->container->get('input'));

        // Create the texture object we want to draw later
        $this->myImageTexture  = new Texture($gl, 'test');
        $this->myImageTexture->loadFromFile(__DIR__ . '/vendor/phpgl/visu/resources/phplogo.png');

        // Create a 2D camera
        $this->camera = new Camera(CameraProjectionMode::orthographic);
        $this->camera->nearPlane = -10;
        $this->camera->farPlane = 10;

        // create a shader to draw the texture
        // this shader will take in a model matrix representing the position and scale of the image
        $this->textureDrawShader = new ShaderProgram($gl);
        $this->textureDrawShader->attach(new ShaderStage(ShaderStage::VERTEX, <<< 'GLSL'
        #version 330 core

        layout (location = 0) in vec3 a_pos;
        layout (location = 1) in vec2 a_uv;

        out vec2 v_uv;

        uniform mat4 u_view;
        uniform mat4 u_projection;
        uniform mat4 u_model;

        // In this example scale represents size in pixels
        // so we need to halve the model scale
        mat4 halfscale = mat4(
            0.5, 0.0, 0.0, 0.0,
            0.0, 0.5, 0.0, 0.0,
            0.0, 0.0, 0.5, 0.0,
            0.0, 0.0, 0.0, 1.0
        );

        void main() {
            v_uv = a_uv; // pass the texture coordinates to the fragment shader
            gl_Position = u_projection * u_view * u_model * halfscale * vec4(a_pos, 1.0);
        }
        GLSL));

        // also attach a simple fragment shader
        $this->textureDrawShader->attach(new ShaderStage(ShaderStage::FRAGMENT, <<< 'GLSL'
        #version 330 core

        in vec2 v_uv;
        out vec4 fragment_color;
        uniform sampler2D u_texture;

        void main() {             
            vec2 uv = vec2(v_uv.x, 1.0 - v_uv.y);
            fragment_color = texture(u_texture, uv);
        }
        GLSL));
        $this->textureDrawShader->link();
    }

    /**
     * Start the game
     * This will begin the game loop
     */
    public function start() : void
    {
        // start the game loop
        $this->container->getTyped(GameLoop::class, 'loop')->start();
    }

    /**
     * Update the games state
     * This method might be called multiple times per frame, or not at all if
     * the frame rate is very high.
     * 
     * The update method should step the game forward in time, this is the place
     * where you would update the position of your game objects, check for collisions
     * and so on. 
     * 
     * @return void 
     */
    public function update() : void
    {
        $window = $this->container->getTyped(Window::class, 'window');

        // poll for new events
        $window->pollEvents();
    }

    /**
     * Render the current game state
     * This method is called once per frame.
     * 
     * The render method should draw the current game state to the screen. You recieve 
     * a delta time value which you can use to interpolate between the current and the
     * previous frame. This is useful for animations and other things that should be
     * smooth with variable frame rates.
     * 
     * @param float $deltaTime
     * @return void 
     */
    public function render(float $deltaTime) : void
    {
        $window = $this->container->getTyped(Window::class, 'window');

        // fetch the render target
        $windowRenderTarget = $window->getRenderTarget();
        $windowRenderTarget->framebuffer()->clearColor = new Vec4(1, 0.2, 0.2, 1.0);

        // retrieve projection and view matrices from the camera
        $viewMatrix = $this->camera->getViewMatrix($deltaTime);
        $projectionMatrix = $this->camera->getProjectionMatrix($windowRenderTarget);

        // construct a rendering pipeline
        $data = new PipelineContainer;
        $pipeline = new RenderPipeline($this->container->get('pipeline_res'), $data, $windowRenderTarget);

        // clear the backbuffer
        $pipeline->addPass(new ClearPass($data->get(BackbufferData::class)->target));

        // create a render pass for our image
        $pipeline->addPass(new CallbackPass(
            'ExampleImagePass',
            // setup (we need to declare who is reading and writing what)
            function(RenderPass $pass, RenderPipeline $pipeline, PipelineContainer $data) {
                $pipeline->writes($pass, $data->get(BackbufferData::class)->target);
            },
            // execute
            function(PipelineContainer $data, PipelineResources $resources) use($viewMatrix, $projectionMatrix)
            {
                // enable our shader and set the uniforms camera uniforms
                $this->textureDrawShader->use();
                $this->textureDrawShader->setUniformMat4('u_view', false, $viewMatrix);
                $this->textureDrawShader->setUniformMat4('u_projection', false, $projectionMatrix);
                $this->textureDrawShader->setUniform1i('u_texture', 0);

                // bind the texture
                $this->myImageTexture->bind(GL_TEXTURE0);

                /** @var QuadVertexArray */
                $quadVA = $resources->cacheStaticResource('quadva', function(GLState $gl) {
                    return new QuadVertexArray($gl);
                });

                // draw the image 5 times
                for ($i=0; $i<5; $i++) {
                    $transform = new Transform;
                    $transform->position = new Vec3(150 + ($i * 200), 350 + sin(glfwGetTime() + $i * 2) * 150, 0);
                    $transform->scale = new Vec3(150, 150, 1);

                    // set the model matrix
                    $this->textureDrawShader->setUniformMat4('u_model', false, $transform->getLocalMatrix());

                    // draw the quad
                    $quadVA->draw();
                }
            }
        ));

        // run the pipeline
        $pipeline->execute(0);

        // swap the backbuffer
        $window->swapBuffers();
    }

    /**
     * Loop should stop
     * This method is called once per frame and should return true if the game loop
     * should stop. This is useful if you want to quit the game after a certain amount
     * of time or if the player has lost all his lives etc..
     * 
     * @return bool 
     */
    public function shouldStop() : bool
    {
        $window = $this->container->getTyped(Window::class, 'window');
        return $window->shouldClose();
    }
}

/**
 * Create a container for the required dependecies
 */
$container = new Container();
$container->set('gl', new GLState);
$container->bindClass('window', Window::class, ['VISU Engine', 1280, 720, new WindowHints]);
$container->bindClass('game', Game::class, ['@container']);
$container->bindClass('loop', GameLoop::class, ['@game']);
$container->bindClass('dispatcher', Dispatcher::class);
$container->bindClass('input', Input::class, ['@window', '@dispatcher']);
$container->bindClass('pipeline_res', PipelineResources::class, ['@gl']);

/**
 * Initalize GLFW and begin
 */
glfwInit();

// load & start the game
$game = $container->get('game');
$game->start();

// clean up glfw
glfwTerminate();
ww9 commented 1 year ago

This is great! Thank you so much for taking the time to write it, document and share. Very kind of you!

With OOP I believe I can encapsulate most low level functionality.

Again, thanks for the quick and comprehensive reply!

I'll get my hands wet during weekend if possible with some simple 2D game. Perhaps a Pong or Breakout game.

ww9 commented 1 year ago

This is totally my fault but just for documentation sake, if it helps someone.

First time I ran php.exe example.phpI got the following error:

PHP Fatal error:  Uncaught Error: Undefined constant "VISU\OS\GLFW_OPENGL_CORE_PROFILE" in E:\dev\playground\phpgame\example.php:243
Stack trace:
#0 {main}
  thrown in E:\dev\playground\phpgame\example.php on line 243

Then I remembered that I forgot to download the DLL and add extension=glfw to php.ini. After doing that it works! I just needed to RTFM 🤣

php_PpNMDngUX3