UNPKG

shaku

Version:

A simple and effective JavaScript game development framework that knows its place!

1,222 lines (865 loc) 100 kB
![Shaku JS](resources/logo-sm.png) ### A simple and effective JavaScript game development framework *that knows its place*! **If you're looking for a package that implements rendering, sounds, assets and input, while keeping it low level and simple, this is the library for you!** (if you're looking for a game engine with editor like Unity, this library is not what you're looking for) Demos & docs: - [Online Demos](https://ronenness.github.io/Shaku/demo/index.html) - [Homepage](https://ronenness.github.io/Shaku/) - [Full API Docs](https://ronenness.github.io/Shaku/docs/index.html) Projects made with *Shaku*: - [HellEscape](https://store.steampowered.com/app/2135030/HellEscape/) - [Game Demo Project](https://ronenness.github.io/Shaku-Demo/) - [GridBender](https://knexator.itch.io/gridbender) - [Another Clone](https://knexator.itch.io/another-clone) - [Catalyst](https://knexator.itch.io/catalyst) - [King of Veggies](https://pinchazumos.itch.io/king-of-veggies) (Want your game listed above? Contact me at ronenness@gmail.com) # Table Of Content - [About](#about) - [Features](#features) - [Installation](#installation) - [Online Demo](#online-demo-projects) - [Using Shaku](#using-shaku) - [Setup](#setup) - [graphics](#graphics) - [Sounds](#sounds) - [Input](#input) - [Assets](#assets) - [Collision](#collision) - [Utils](#utils) - [Miscellaneous](#miscellaneous) - [Advanced Topics](#advanced-topics) - [Shaku on NodeJS](#shaku-on-nodejs) - [Build Shaku](#build-shaku) - [Changes](#changes) - [License](#license) # About *Shaku* is a JavaScript framework for web games development that emphasize **simplicity**, **flexibility** and **freedom**. It's pretty low level and designed to be used as the foundations for a higher-level game engine, or used directly for game development. Kind of like MonoGame, RayLib or libGDX. Let's take a quick look at how we make a game main loop with *Shaku*: ```js // Init code goes here, we'll review it later.. // main loop (do updates, render and request next step) function step() { // start a new frame and clear screen Shaku.startFrame(); Shaku.gfx.clear(Shaku.utils.Color.cornflowerblue); // draw a sprite using the spritebatch spritesBatch.begin(); let position = new Shaku.utils.Vector2(400, 300); let size = new Shaku.utils.Vector2(100, 100); spritesBatch.drawQuad(texture, position, size); spritesBatch.end(); // end frame and request next step Shaku.endFrame(); Shaku.requestAnimationFrame(step); } ``` ## Main Features *Shaku* provides the following key features: * Ultra-fast WebGL rendering engine. * Assets loader to fetch textures, sounds, music, JSON, and other resources. * Texture Atlas builder to combine textures efficiently at runtime. * Collision detection. * Custom effects, text rendering, render targets, batching and other graphics-essentials. * Input manager for simple touch, mouse, gamepad and keyboard *state-based* input (instead of events). * Sound effects, music, tracks mixer, pitch and everything you need for sfx. * Basic utilities such as Vectors, Matrices, 2D Shapes, GameTime and more. * Advance utilities such as Animators, Path Finder, Perlin generator and other super useful stuff. * *All packed in a tiny lib with no external dependencies*! A single minified 150Kb JS is all you need to use Shaku.* If you want to experiment with *Shaku* while reading the docs, you can check out this [Sandbox Demo](https://ronenness.github.io/Shaku/demo/sandbox.html). See the [demos assets](demo/assets) folder to see which assets you can use for the sandbox (or load assets from external sources). ![Sandbox](resources/sandbox.jpg) ## Installation Using *Shaku* is super easy! 1. Get `shaku.js` or `shaku.min.js` from the `dist/` folder and include it in your page (or use npm to get it). 2. Init *Shaku* and append the canvas to your document (or init *Shaku* on an existing canvas). 3. Write a game loop method starting with `Shaku.startFrame()` and ending with `Shaku.endFrame()` and `requestAnimationFrame()` to get next frame. To get *Shaku* via NPM: ```npm install shaku``` ### HTML Boilerplate The following is a boilerplate HTML with *Shaku* running an empty game main loop: ```html <!DOCTYPE html> <html> <head> <title>Shaku Example</title> <script src="dist/shaku.js"></script> </head> <body> <script> (async function runGame() { // init shaku await Shaku.init(); // add shaku's canvas to document and set resolution to 800x600 document.body.appendChild(Shaku.gfx.canvas); Shaku.gfx.setResolution(800, 600, true); // TODO: LOAD ASSETS AND INIT GAME LOGIC HERE // do a single main loop step and request next step inside function step() { // start a new frame and clear screen Shaku.startFrame(); Shaku.gfx.clear(Shaku.utils.Color.cornflowerblue); // TODO: PUT YOUR GAME UPDATES AND RENDERING HERE // end frame and invoke the next step in 60 FPS rate (or more, depend on machine and browser) Shaku.endFrame(); Shaku.requestAnimationFrame(step); } // start the main loop step(); })(); </script> </body> </html> ``` You can find the above HTML file [here](html_boilerplate.html). # Online Demo Projects Online demo projects can be found [here](https://ronenness.github.io/Shaku/demo/index.html). They demonstrate basic to advanced *Shaku* features. ![Demo-2](resources/demo-2.png) # Using Shaku *Shaku*'s API mostly consist of five main managers, each solve a different domain of problems in gamedev: *graphics*, *sounds*, *assets*, *collision* and *input*. In this doc we'll explore these managers and cover the most common use cases with them. If you want to dive deep into the API, you can check out the [API Docs](docs/index.md), or browse the code. ## Setup Everything in *Shaku* is located under the `Shaku` object. Since the initialization process and asset loading is mostly asynchronous operations, its best to wrap the init code in an `async` method and utilize `await` calls to simplify the code. A common *Shaku* initialization code will look something like this: ```js (async function runGame() { // init shaku. // for pixel art games its best to set antialias=false before init. Shaku.gfx.setContextAttributes({antialias: false}); await Shaku.init(); // add shaku's canvas to document and set resolution to 800x600. // this will set the canvas and renderer size. document.body.appendChild(Shaku.gfx.canvas); Shaku.gfx.setResolution(800, 600, true); // TODO: add code to load assets and init game logic here. // game main loop function step() { // start frame and clear the screen Shaku.startFrame(); Shaku.gfx.clear(Shaku.utils.Color.cornflowerblue); // TODO: add game logic code here Shaku.endFrame(); Shaku.requestAnimationFrame(step); } step(); })(); ``` Let's go over the code above line by line: * `Shaku.gfx.setContextAttributes({antialias: false})` will disable smooth filtering, and keep everything crispy and pixelated. * `await Shaku.init()` will initialize all *Shaku*'s managers. * `document.body.appendChild(Shaku.gfx.canvas)` add the canvas created by *Shaku* to the document body (you can also use an existing canvas instead). * `Shaku.gfx.setResolution(800, 600, true)` will set both canvas size and renderer size to 800x600 px. * `Shaku.startFrame()` must be called at the beginning of every game frame. * `Shaku.gfx.clear(Shaku.utils.Color.cornflowerblue)` will clear the canvas to blue-ish color. * `Shaku.endFrame()` must be called at the end of every game frame. * `Shaku.requestAnimationFrame(step)` will request next frame when its time to render screen again, keeping updates() rate at about 60 FPS (or more if the browser and machine allows it). As you can see from the example above, our step() method represent a single iteration in our game main loop. It handles both rendering and updates. Now we can start using *Shaku*'s managers, mostly between the `startFrame()` and `endFrame()` calls. ## Graphics Let's start exploring the APIs from the graphics manager, accessed by `Shaku.gfx`. In *Shaku* we use batches to render everything. These batches collect multiple draw calls and batch them together into a single GPU call. This way of rendering is essential for performance, but it has some limitations. For example, you can only only batch rendering with the same texture, blend mode, and shaders. This doc don't cover the entirely of the API, only the main parts of it. To see the full API, check out the [API docs](docs/gfx_gfx.md). ### Drawing Textures To draw textures (also known as 2d sprites) we use a `SpriteBatch` renderer. This renderer batch together 2d quads and other shapes with textures on them. Let's see a minimal code example to render a texture on screen: ```js // this part comes after we init shaku, but still outside the main loop: // during the init phase, we create a spritebatch and load a texture to draw const texture = await Shaku.assets.loadTexture('<your texture path here..>'); const spriteBatch = new Shaku.gfx.SpriteBatch(); // this part comes between the Shaku.startFrame() and the Shaku.endFrame() calls: // draw the texture with the batch spritesBatch.begin(); let position = new Shaku.utils.Vector2(400, 300); let size = new Shaku.utils.Vector2(100, 100); spritesBatch.drawQuad(texture, position, size); spritesBatch.end(); ``` Pretty simple, eh? Now let's draw with some more parameters: ```js spritesBatch.begin(); let position = new Shaku.utils.Vector2(100, 125); let size = new Shaku.utils.Vector2(32, 64); let sourceRect = new Shaku.utils.Rectangle(32, 0, 32, 64); let color = Shaku.utils.Color.red; let rotation = Math.PI / 2; let origin = new Shaku.utils.Vector2(0.5, 1); let skew = new Shaku.utils.Vector2(32, 0); Shaku.gfx.drawQuad(texture, position, size, sourceRect, color, rotation, origin, skew); spritesBatch.end(); ``` When beginning a batch, you can set different blend modes and effects. For example: ```js spritesBatch.begin(Shaku.gfx.BlendModes.Additive, myCustomEffect); ``` We'll learn more about effects later, don't worry about it for now. A simple rendering demo can be found [here](https://ronenness.github.io/Shaku/demo/gfx_draw.html). #### When does a GPU draw call is made? The `SpriteBatch` will call the GPU to draw everything on three different occasions: 1. When `spritesBatch.end()` is called. 2. When you change the texture. 3. If the batch overflows and can't contain any more drawings. As you can see number #2 is something we need to watch out for. Texture Atlases are great way to reduce draw calls, and when possible, you should sort your rendering order by textures. We'll learn more about Texture Atlases later. To learn more about Sprite Batches and see what else you can do with them, its recommended to check out the docs. For example you can control pixel aligning, buffers size, how to handle overflow, etc. #### Sprites `Sprites` are entities that store all rendering parameters required to make a draw call. It's just a more object-based approach to draw stuff. Lets create a sprite and set some of its fields: ```js // this part comes after we init shaku, but still outside the main loop: // load texture and create sprite let texture = await Shaku.assets.loadTexture('assets/my_texture.png'); let sprite = new Shaku.gfx.Sprite(texture); // set some fields sprite.position.set(100, 125); sprite.size.set(32, 64); sprite.sourceRectangle = new Shaku.utils.Rectangle(32, 0, 32, 64); sprite.color = Shaku.utils.Color.red; sprite.rotation = Math.PI / 2; sprite.origin.set(0.5, 1); // this part comes between the Shaku.startFrame() and the Shaku.endFrame() calls: // draw the sprite in a sprite batch spritesBatch.begin(); Shaku.gfx.drawSprite(sprite); spritesBatch.end(); ``` #### Sprites Group As the name implies, a sprites group is a collection of sprites. Let's see how we use it: ```js // this part comes after we init shaku, but still outside the main loop: // create a group let group = new Shaku.gfx.SpritesGroup(); // set group position, scale and rotation // these transformations will affect all sprites in group group.position.set(100, 100); group.rotation = Math.PI / 2; group.scale.set(2, 2); // add some sprites to the group let texture = await Shaku.assets.loadTexture('assets/my_texture.png'); for (let i = 0; i < 3; ++i) { let sprite = new Shaku.gfx.Sprite(texture); sprite.position = new Shaku.utils.Vector2(i * 100, 0); sprite.size = new Shaku.utils.Vector2(50, 50); sprite.origin = new Shaku.utils.Vector2(0, 0); group.add(sprite); } // this part comes between the Shaku.startFrame() and the Shaku.endFrame() calls: // draw group spritesBatch.begin(); Shaku.gfx.drawSpriteGroup(group); spritesBatch.end(); ``` The advantage of groups is that you can apply common transformations on all the sprites in the group around the same origin point. Its also slightly more efficient in some cases. A demo page that draw with sprites group can be found [here](https://ronenness.github.io/Shaku/demo/gfx_sprites_group.html). ### Drawing 3D Sprites *Shaku* provides a simple 3D Sprite Batch renderer. This is useful for simple 3D stuff like this: ![3D Sprites](resources/demo-3.png) Let's take a look at a basic 3D sprites example: ```js // this part comes after we init shaku, but still outside the main loop: // create a 3d sprite and set a default perspective camera. // check out setPerspectiveCamera() arguments to see more options. let spritesBatch3d = new Shaku.gfx.SpriteBatch3D(); spritesBatch3d.setPerspectiveCamera(); // this part comes between the Shaku.startFrame() and the Shaku.endFrame() calls: // begin drawing 3d sprites spritesBatch3d.begin(); // set view matrix (camera position and where we look at) spritesBatch3d.setViewLookat( new Shaku.utils.Vector3(0, 500, 600), new Shaku.utils.Vector3(0, 0, 0) ); // draw 3d quad from 4 vertices const v1 = (new Shaku.gfx.Vertex()) .setPosition(new Shaku.utils.Vector3(-spriteSize.x / 2, 0, 0)) .setTextureCoords(new Shaku.utils.Vector2(0, 1)); const v2 = (new Shaku.gfx.Vertex()) .setPosition(new Shaku.utils.Vector3(spriteSize.x / 2, 0, 0)) .setTextureCoords(new Shaku.utils.Vector2(1, 1)); const v3 = (new Shaku.gfx.Vertex()) .setPosition(new Shaku.utils.Vector3(-spriteSize.x / 2, spriteSize.y, 0)) .setTextureCoords(new Shaku.utils.Vector2(0, 0)); const v4 = (new Shaku.gfx.Vertex()) .setPosition(new Shaku.utils.Vector3(spriteSize.x / 2, spriteSize.y, 0)) .setTextureCoords(new Shaku.utils.Vector2(1, 0)); spritesBatch3d.drawVertices(texture, [v1, v2, v3, v4]); // end rendering spritesBatch3d.end(); ``` ### Drawing Shapes *Shaku* also provides a way to draw some basic 2D shapes: ```js // this part comes after we init shaku, but still outside the main loop: // create shapes batch to render 2d shapes let shapesBatch = new Shaku.gfx.ShapesBatch(); // this part comes between the Shaku.startFrame() and the Shaku.endFrame() calls: // start drawing shapes shapesBatch.begin(); // draw a circle in the center of screen with radius of 400. its center is red, its outter parts are blue, and it has 32 segments. shapesBatch.drawCircle(new Shaku.utils.Circle(Shaku.gfx.getCanvasSize().div(2), 400), Shaku.utils.Color.red, 32, Shaku.utils.Color.blue); // draw a rectangle at offset 100,100 and size 256,256, with red color, that rotates over time let rotation = Shaku.gameTime.elapsed; shapesBatch.drawRectangle(new Shaku.utils.Rectangle(100, 100, 256, 256), Shaku.utils.Color.red, rotation); // draw everything on screen shapesBatch.end(); ``` Similar to `ShapesBatch`, there's also a `LinesBatch` renderer to draw just the outline of shapes (or a string of lines from vertices). ### Drawing Text *Shaku* support rendering text from *Font Texture* (also known as Bitmap Fonts). These fonts store the glyphs as pixels data, and render the text as sprites. You don't need to prepare the Font Textures upfront; *Shaku* generates them at runtime from regular TTF fonts. For example, *Shaku* generated the following font texture for one of the [online demos](https://ronenness.github.io/Shaku/demo/gfx_draw_text.html): ![font texture](resources/font-texture.png) Let's take a look at how we generate a Sprite Font and render some text with it: ```js // this part comes after we init shaku, but still outside the main loop: // load font texture // note: the fontName argument MUST match the font name defined in the ttf file. let fontTexture = await Shaku.assets.loadFontTexture('assets/DejaVuSansMono.ttf', {fontName: 'DejaVuSansMono'}); // create text sprite batch let textSpriteBatch = new Shaku.gfx.TextSpriteBatch(); // generate a text group to render in white, aligned to the left, and positioned at 100,100. let textGroup = Shaku.gfx.buildText(fontTexture, "Hello World!\nThis is second line.", 24, Shaku.utils.Color.white, Shaku.gfx.TextAlignments.Left); textGroup.position.set(100, 100); // this part comes between the Shaku.startFrame() and the Shaku.endFrame() calls: // draw the text textSpriteBatch.begin(); textSpriteBatch.drawText(textGroup); textSpriteBatch.end(); // you can also draw the text with outlines: textSpriteBatch.outlineWeight = 0.75; textSpriteBatch.outlineColor = Shaku.utils.Color.black; textSpriteBatch.begin(); textSpriteBatch.drawText(textGroup); textSpriteBatch.end(); ``` When loading the `FontTexture` you can provide additional parameters, to learn more about them check out the [API Docs](docs/assets_font_texture_asset.md). Note that *Shaku* also support hi-res MSDF Font Textures, but it can't generate them at runtime. To see how to use MSDF font textures, check out [this demo](https://ronenness.github.io/Shaku/demo/gfx_draw_text_msdf.html). ### Render Targets Render Targets provide a way to draw *on textures* instead of directly on screen, and then draw these textures on screen. This technique is useful for post processing effects, or to implement virtual resolution (by drawing on a constant-sized texture and then present it on screen). For example [the following game](https://store.steampowered.com/app/2135030/HellEscape/) is built with *Shaku*, and uses Render Targets to implement the 2D lightings you see here: ![HellEscape](resources/hellescape.png) A render target in essence is just a texture asset you can draw on, created like this: ```js let renderTarget = await Shaku.assets.createRenderTarget('_my_render_target', width, height); ``` Then you can start drawing on it by setting it as the active render target: ```js // set render target Shaku.gfx.setRenderTarget(renderTarget); // draw some stuff here... // these renderings will appear on the texture instead of the canvas. // reset render target so we can continue drawing on screen / canvas Shaku.gfx.setRenderTarget(null); ``` And finally we can use the render target just like we would use any other texture: ```js spritesBatch.begin(); let position = new Shaku.utils.Vector2(400, 300); let size = new Shaku.utils.Vector2(100, 100); spritesBatch.drawQuad(renderTarget, position, size); spritesBatch.end(); ``` A demo that uses render targets can be found [here](https://ronenness.github.io/Shaku/demo/gfx_render_target.html). ### Cameras The camera object define two key properties: - Viewport: the region of the canvas we render on. - Projection: offset and scale of everything we draw on the canvas. By default, *Shaku* will use a camera with no offset and scale of x1, and a viewport that covers the entire canvas. In other words, an identity camera that won't affect anything. To change the default camera: ```js // create camera object let camera = Shaku.gfx.createCamera(); // set offset and use the camera (call this before rendering) camera.orthographicOffset(cameraOffset); Shaku.gfx.applyCamera(camera); ``` And if later you want to reset camera back to default, you can call the following method: ```js Shaku.gfx.resetCamera(); ``` For more details, check out the [Camera object API](docs/gfx_camera.md) in the docs. A demo page that uses cameras can be found [here](https://ronenness.github.io/Shaku/demo/miscs_tilemap.html). ### Texture Atlas Every time you change the texture, a draw call is made to the GPU. This means that if you render 100 different textures in a row you will suffer 100 GPU draw calls (and 100 textures switching), which is very ineffective in terms of performance. To solve this issue video games often use a *Texture Atlas*, which is a single large texture containing multiple smaller textures. That way, we can reduce texture switching and draw calls. Creating a Texture Atlas manually is a tedious work, and as you add more and more textures sometimes you end up with inconvenient 'holes' that makes the atlas less space-efficient. For that reason, *Shaku* has a built-in Atlas builder that helps you generate an efficient Texture Atlas at runtime: ```js // all source textures URLs const sourceUrls = [ 'assets/stone_wall.png', 'assets/grass.png', 'assets/tree.png', ... ]; // create a texture atlas named 'my-texture-atlas'. // you can also limit its dimensions if needed to. let textureAtlas = await Shaku.assets.createTextureAtlas('my-texture-atlas', sourceUrls); // then you can use the texture atlas like this: // extract one of the textures // textureInAtlas is an object with `texture` and `sourceRectangle`. let textureInAtlas = textureAtlas.getTexture('assets/stone_wall.png'); // draw the texture at 100,100 let size = textureInAtlas.sourceRectangle.getSize(); spritesBatch.begin(); spritesBatch.drawQuad(textureInAtlas, new Shaku.utils.Vector2(100, 100), size); spritesBatch.end(); ``` Note that a texture atlas is not necessarily a single texture; Since there's a GPU limit for max textures size, the atlas may generate more than one texture. That's why when you call `getTexture()` the object don't return just source rectangle, but a texture asset as well. ### Effects Effects provide a way to change the shaders *Shaku* uses to draw textures and shapes. When implementing an effect, you need to follow four steps: 1. Write or use an existing *vertex shader code*. 2. Write or use an existing *fragment shader code*. 3. Define your shaders Uniforms. 4. Define your shaders Attributes. And optionally, you can instruct *Shaku* to use custom attributes and uniforms internally, so you won't need to explicitly set them. More on that later. Lets write a simple custom effect and then review and explain the code: ```js // define a custom effect class MyEffect extends Shaku.gfx.SpritesEffect { /** * Override the fragment shader for our custom effect. */ get fragmentCode() { const fragmentShader = ` #ifdef GL_ES precision highp float; #endif uniform sampler2D mainTexture; uniform float elapsedTime; varying vec2 v_texCoord; varying vec4 v_color; void main(void) { gl_FragColor = texture2D(mainTexture, v_texCoord) * v_color; gl_FragColor.r *= sin(v_texCoord.y * 10.0 + elapsedTime) + 0.1; gl_FragColor.g *= sin(1.8 + v_texCoord.y * 10.0 + elapsedTime) + 0.1; gl_FragColor.b *= sin(3.6 + v_texCoord.y * 10.0 + elapsedTime) + 0.1; gl_FragColor.rgb *= gl_FragColor.a; } `; return fragmentShader; } /** * Override the uniform types dictionary to add our custom uniform type. */ get uniformTypes() { let ret = super.uniformTypes; ret['elapsedTime'] = { type: Shaku.gfx.Effect.UniformTypes.Float }; return ret; } } ``` Before reading on, can you guess what this effect do? This effect recieve *elapsed time* as a uniform (called 'elapsedTime') with type `flot`, and animate the texture colors based on the current time value. Since every component gets a different offset from the start, it will create a rainbow-like colors effect. ![Custom Effects](resources/custom_effects.png) A demo page with the above effect can be found [here](https://ronenness.github.io/Shaku/demo/gfx_custom_effects.html). Now lets review the code. 1. First, notice we're extending the `Shaku.gfx.SpritesEffect` class. This is the default effect *Shaku* uses for sprites, and by inheriting from it we can skip implementing the vertex shader and just use the default one. It also covers the basic attributes binding for vertices data. If you want to create a brand new effect that doesn't use anything from the built-in sprites effect, extend `Shaku.gfx.Effect` instead. 2. Next we have `get fragmentCode()`, that returns the fragment shader code to compile for this effect. There is also a `get fragmentCode()` getter for the vertex shader code, but as mentioned earlier we relay on the default sprites vertex shader so we don't need to implement it. 4. And finally, `get uniformTypes()` returns a dictionary with uniforms we want to set in the effect. Note that we do `let ret = super.uniformTypes;` to extend the base class uniforms so we won't miss out on any functionality from the basic effect. Now we can start using this effect with our Sprites Batch: ```js // create the custom effect let effect = new MyEffect(); // update effect elapsed time and render with it // the setter `effect.uniforms.elapsedTime` exists because we defined it in `get uniformTypes()` effect.uniforms.elapsedTime(Shaku.gameTime.elapsed); spritesBatch.begin(undefined, effect); spritesBatch.drawQuad(texture, new Shaku.utils.Vector2(100, 100), 400); spritesBatch.end(); ``` #### Default Effect To learn more about effects, lets review the [default built-in effect](https://github.com/RonenNess/Shaku/blob/main/src/gfx/effects/sprites.js) *Shaku* normally uses for sprites with vertex color: ```js // vertex shader code const vertexShader = ` attribute vec3 position; attribute vec2 uv; attribute vec4 color; uniform mat4 projection; uniform mat4 world; varying vec2 v_texCoord; varying vec4 v_color; void main(void) { gl_Position = projection * world * vec4(position, 1.0); gl_PointSize = 1.0; v_texCoord = uv; v_color = color; } `; // fragment shader code const fragmentShader = ` #ifdef GL_ES precision highp float; #endif uniform sampler2D mainTexture; varying vec2 v_texCoord; varying vec4 v_color; void main(void) { gl_FragColor = texture2D(mainTexture, v_texCoord) * v_color; gl_FragColor.rgb *= gl_FragColor.a; } `; /** * Default basic effect to draw 2d sprites. */ class SpritesEffect extends Effect { /** @inheritdoc */ get vertexCode() { return vertexShader; } /** @inheritdoc */ get fragmentCode() { return fragmentShader; } /** @inheritdoc */ get uniformTypes() { return { [Effect.UniformBinds.MainTexture]: { type: Effect.UniformTypes.Texture, bind: Effect.UniformBinds.MainTexture }, [Effect.UniformBinds.Projection]: { type: Effect.UniformTypes.Matrix, bind: Effect.UniformBinds.Projection }, [Effect.UniformBinds.World]: { type: Effect.UniformTypes.Matrix, bind: Effect.UniformBinds.World }, [Effect.UniformBinds.View]: { type: Effect.UniformTypes.Matrix, bind: Effect.UniformBinds.View } }; } /** @inheritdoc */ get attributeTypes() { return { [Effect.AttributeBinds.Position]: { size: 3, type: Effect.AttributeTypes.Float, normalize: false, bind: Effect.AttributeBinds.Position }, [Effect.AttributeBinds.TextureCoords]: { size: 2, type: Effect.AttributeTypes.Float, normalize: false, bind: Effect.AttributeBinds.TextureCoords }, [Effect.AttributeBinds.Colors]: { size: 4, type: Effect.AttributeTypes.Float, normalize: false, bind: Effect.AttributeBinds.Colors }, }; } } ``` As you can see above we use *attributes* for vertices data (position, texture coords, and colors), and uniforms for texture and transformation matrices (projection, world, and view). **Binds** You may have noticed an additional parameter added to the attributes and uniforms: `bind`. Binding is a way for us to tell *Shaku* how to handle attributes and uniforms we want to use for basic stuff, like vertices color or texture coords, so that *Shaku* can set these values internally when preparing the batch. For example, when we added the bind `Effect.AttributeBinds.Position` to the attributes named `[Effect.AttributeBinds.Position]`, we instructed *Shaku* to send the vertices position data into this attribute from our vertex shader, which is called `position`. If you want to define an effect and call this attribute a different name, for example 'attr_vertexPos', you can just set it like this in your effect: `attr_vertexPos: { size: 3, type: Effect.AttributeTypes.Float, normalize: false, bind: Effect.AttributeBinds.Position },` **The following binds are valid for uniform types**: - *MainTexture*: bind uniform to be used as the main texture. - *Color*: bind uniform to be used as a main color. - *Projection*: bind uniform to be used as the projection matrix. - *World*: bind uniform to be used as the world matrix. - *View*: bind uniform to be used as the view matrix. - *UvOffset*: bind uniform to be used as UV offset. - *UvScale*: bind uniform to be used as UV scale. - *OutlineWeight*: bind uniform to be used as outline weight. - *OutlineColor*: bind uniform to be used as outline color. - *UvNormalizationFactor*: bind uniform to be used as factor to normalize uv values to be 0-1. - *TextureWidth*: bind uniform to be used as texture width in pixels. - *TextureHeight*: bind uniform to be used as texture height in pixels. **And the following binds are valid for attribute types**: - *Position*: bind attribute to be used for vertices position array. - *TextureCoords*: bind attribute to be used for texture coords array. - *Colors*: bind attribute to be used for vertices colors array. Anything else, you'll just need to set yourself. ### Matrices Matrices are used to transform vertices. They can express rotation, scale, translation, and camera view and projection. You can access the Matrix class with `Shaku.gfx.Matrix`. Let's take a look at a basic example of using a matrix to move the position of 3d vertices: ```js // create vertices const spriteSize = new Shaku.utils.Vector2(spriteTexture.width * 1.5, spriteTexture.height * 1.5); const v1 = (new Shaku.gfx.Vertex()) .setPosition(new Shaku.utils.Vector3(-spriteSize.x / 2, 0, 0)) .setTextureCoords(new Shaku.utils.Vector2(0, 1)); const v2 = (new Shaku.gfx.Vertex()) .setPosition(new Shaku.utils.Vector3(spriteSize.x / 2, 0, 0)) .setTextureCoords(new Shaku.utils.Vector2(1, 1)); const v3 = (new Shaku.gfx.Vertex()) .setPosition(new Shaku.utils.Vector3(-spriteSize.x / 2, spriteSize.y, 0)) .setTextureCoords(new Shaku.utils.Vector2(0, 0)); const v4 = (new Shaku.gfx.Vertex()) .setPosition(new Shaku.utils.Vector3(spriteSize.x / 2, spriteSize.y, 0)) .setTextureCoords(new Shaku.utils.Vector2(1, 0)); // create a translation matrix const position = new Shaku.utils.Vector3(10, 100, 50); const matrix = Shaku.gfx.Matrix.translate(position.x, position.y, position.z); // transform the vertices with the matrix and draw them in a 3d batch const vertices = [Shaku.gfx.Matrix.transformVertex(matrix, v1), Shaku.gfx.Matrix.transformVertex(matrix, v2), Shaku.gfx.Matrix.transformVertex(matrix, v3), Shaku.gfx.Matrix.transformVertex(matrix, v4)]; spritesBatch3d.begin(); spritesBatch3d.drawVertices(spriteTexture, vertices); spritesBatch3d.end(); ``` To learn more about matrices, check out the [API docs](docs/gfx_matrix.md). ### Conclusion In this chapter we learned about the `Gfx` manager and how to use it to create batches and render basic things. What we covered here is just the common use cases, to learn more about what you can draw with *Shaku* check out the [online demos](https://ronenness.github.io/Shaku/demo/index.html) or the [online docs](https://ronenness.github.io/Shaku/docs/index.html). ## Sounds We finally reached our second manager, the sounds manager, which is accessed by `Shaku.sfx`. This doc don't cover the entirely of the API, only the main parts of it. To see the full API, check out the [API docs](docs/sfx_sfx.md). ### Play Sound To play an audio asset once: ```js // load the sound file let sound = await Shaku.assets.loadSound('assets/my_sound_file.ogg'); // play sound let volume = 0.85; let pitch = 1; Shaku.sfx.play(sound, volume, pitch); ``` Note that due to browsers security limitations, you won't be able to play any sound effect until the user interacted with the page with mouse, touch, gamepad, or keyboard. This is not something *Shaku* can solve, as this is by design and enforced by the browsers themselves. You can, however, preload your sound assets before that. ### Sound Instances A sound instance is a way to create a sound object that lives on after it was played, and use it as many times as you like. Its more efficient to reuse instances than to call `play()` multiple times, but its usually an unnecessary optimization as this will rarely be your bottleneck in terms of performance. To create a sound instance: ```js // load sound asset and create instance let sound = await Shaku.assets.loadSound('assets/my_sound_file.ogg'); let soundInstance = Shaku.sfx.createSound(sound); // play the sound soundInstance.play(); ``` The sound instance have the following properties: * *play()*: Start playing the sound. * *stop()*: Stop playing the sound and reset time to the beginning. * *pause()*: Stop playing sound but keep current time. * *loop*: Set if should play in loop or not. * *volume*: Set volume. * *currentTime*: Set current time. * *duration*: Get sound duration. * *paused*: Is the sound currently paused? * *playing*: Is the sound currently playing? * *finished*: Did the sound finish playing? * *preservesPitch*: Set if to preserve pitch while changing playback rate. * *playbackRate*: Set playback rate. A demo page that play sounds can be found [here](https://ronenness.github.io/Shaku/demo/sfx_basic_audio.html). ### Sounds Mixer Sound Mixer is a utility class to mix and fade sounds. It's very useful for music transitioning. For example, the following code will create a mixer that transition from music1 to music2, while allowing tracks to overlap (if overlap is false, the mixer will first fade-out music1 completely, and only then begin fading-in music2): ```js // load two music tracks let music1 = await Shaku.assets.loadSound('assets/music1.ogg'); let music2 = await Shaku.assets.loadSound('assets/music2.ogg'); // transition between the tracks let overlap = true; let mixer = new Shaku.sfx.SoundMixer(Shaku.sfx.createSound(music1), Shaku.sfx.createSound(music2), overlap); ``` To run the mixer you then need to call the following inside every step of your game main loop: ```js // Shaku.gameTime.delta is transition speed (delta = time between frames, in seconds) mixer.updateDelta(Shaku.gameTime.delta); ``` And the following code will create a mixer to fade music in without fading anything out: ```js let mixer = new Shaku.sfx.SoundMixer(null, Shaku.sfx.createSound(music2), true); ``` Or, we can just fade music out without fading anything in: ```js let mixer = new Shaku.sfx.SoundMixer(Shaku.sfx.createSound(music1), null, true); ``` If you want to set the mixer to a specific point in time, you can call `update()` with the desired progress from 0.0 to 1.0: ```js let mixProgress = 0.5; // valid range = 0-1 mixer.update(mixProgress); ``` A demo page with sounds mixer can be found [here](https://ronenness.github.io/Shaku/demo/sfx_sound_mixer.html). ### Conclusion In this chapter we learned about the `Sfx` manager and how to use it to play sound effects and music. What we covered here is just the common use cases, to learn more about sound effects with pitch, volume, playback speed and other effects *Shaku* support, check out the [online demos](https://ronenness.github.io/Shaku/demo/index.html) or the [online docs](https://ronenness.github.io/Shaku/docs/index.html). ## Input The input manager provides an API to query Keyboard, Mouse, Touch and Gamepad input. To access the Input manager we use `Shaku.input`. This doc don't cover the entirely of the API, only the main parts of it. To see the full API, check out the [API docs](docs/input_input.md). ### States Queries The input manager have five main query method for key states, which can be mouse buttons, touch, keyboard keys, or gamepad buttons. These five methods are the ones you'll use the most: - **down**: return true if the button / key is currently held down. - **released**: return true if the button / key was released in this very update frame. - **pressed**: return true if the button / key was pressed down in this very update frame. - **doublePressed**: like pressed, but will only trigger after a second quick press. - **doubleReleased**: like released, but will only trigger after a second quick release. For example, a double-click with the mouse left button can be detected like this: `Shaku.input.doublePressed('mouse_left')` (or `doubleReleased` if you prefer to only consider double click if used released the key). In addition, all the methods above also accept an array instead of a single key code, to test if any of them match the condition. For example, the following will check if 'w' or arrow up is down: `Shaku.input.down(['w', 'up_arrow'])`. ![Input Demo](resources/input-demo.png) A demo page to demonstrate the input manager can be found [here](https://ronenness.github.io/Shaku/demo/input_basic_input.html). ### Keyboard All keyboard keys are listed under `Shaku.input.KeyboardKeys`. To query keyboard keys you can just use their names with any of the main state query methods. For example: ```js if (Shaku.input.down('left') || Shaku.input.down('a')) { // move player left } ``` In addition there are some useful keyboard-specific getters you can use: `shiftDown`, `ctrlDown`, `altDown`, `anyKeyPressed`, `anyKeyDown`. ### Mouse To get a mouse button state use the 'mouse_' prefix, followed by the button key (left, right, middle). For example to check if left mouse button is down: ```js if (Shaku.input.down('mouse_left')) { // mouse left button is down } ``` #### Mouse Position To get mouse position use: ```js // returns a Vector2 instance let mousePos = Shaku.input.mousePosition; ``` And to get mouse position change from last frame: ```js // returns a Vector2 instance let mouseDelta = Shaku.input.mouseDelta; ``` **Wait, mouse position is wrong?** By default the input manager will attach events to the *entire document*, meaning that mouse position will be relative to the top-left corner of the web page. If your game canvas does not cover the entire screen, mouse offset will feel wrong because it won't be relative to your canvas top-left corner. You might expect to get 0,0 if you click on the canvas top-left corner, but that would only happen if the canvas starts the the page top-left corner. To solve this, you can instruct the input manager to attach events to the main canvas (or any other element for that matter). This will also make the input manager only work when the canvas is focused, which is useful when combining *Shaku* with HTML UI. To set the Input manager target element, run the following command *before initializing Shaku*: ```js Shaku.input.setTargetElement(() => Shaku.gfx.canvas); ``` Note that we use a callback and not `Shaku.gfx.canvas` directly, as `Shaku.gfx.canvas` will be undefined until we call `Shaku.init`, which is when we must call `setTargetElement()`. #### Mouse Wheel You can query mouse wheel delta with `Shaku.input.mouseWheel`, or get just the mouse wheel direction (if the user scrolls up or down) with `Shaku.input.mouseWheelSign` (will be 0 if wheel is not used this frame). ### Touch By default, *Shaku* will delegate Touch input to Mouse input, so you can use the same key codes for desktop and mobile. This means that Touch start will generate a mouse click input ('mouse_left' code), and when the user drags touch input across the device, the Touch position will update as the Mouse position. To disable this behavior, you can set `Shaku.input.delegateTouchInputToMouse` to `false`. If you choose not to delegate Touch input to Mouse, you can use the following methods to query Touch input: - `Shaku.input.touchPosition`: Get touch last position. - `Shaku.input.touching`: True while the user is touching the device screen. - `Shaku.input.touchStarted`: True if touch input started during this update frame. - `Shaku.input.touchEnded`: True if touch input ended during this update frame. You can get `down`, `pressed`, and `released` state for touch input too, similar to how you'd use a keyboard key or mouse button, but with `touch` as the key code: ```js if (Shaku.input.down('touch')) { // user is touching the screen } if (Shaku.input.released('touch')) { // touch was just released } if (Shaku.input.pressed('touch')) { // touch was just pressed } ``` ### Gamepad You can query Gamepad sticks and buttons with the input manager. Note that gamepads only work after the user press any gamepad button or move the stick. This is due to browsers security limitations. To query connected gamepad ids use: ```js // all ids: console.log("Gamepads: ", Shaku.input.gamepadIds()); // by index: console.log("Gamepad 2 id: ", Shaku.input.gamepadId(2)); // default gamepad (lowest connected index): console.log("Default gamepad id: ", Shaku.input.gamepadId()); ``` And to get a gamepad state object, use: ```js let gamepad = Shaku.input.gamepad(0); // <-- 0 is the index of the gamepad to get, or leave out this param for default gamepad ``` Every gamepad state object will have the following properties: - `gamepad.axis1`: Vector2 with axis1 current value. - `gamepad.axis2`: Vector2 with axis2 current value. - `gamepad.buttonsCount`: How many buttons this gamepad has. - `gamepad.button(index)`: Get the state of a button (up / down). - `gamepad.id`: Gamepad id. - `gamepad.mapping`: Gamepad mapping type. If the gamepad has a standard mapping, `gamepad.isMapped` will be set to true, and the following properties will also appear: - `gamepad.leftStick`: Left stick state (same as axis1). - `gamepad.rightStick`: Left stick state (same as axis2). - `gamepad.leftStickPressed`: Is left stick pressed? - `gamepad.rightStickPressed`: Is right stick pressed? - `gamepad.leftButtons`: Left side buttons cluster (top, bottom, left, right). - `gamepad.rightButtons`: Right side buttons cluster (top, bottom, left, right). - `gamepad.centerButtons`: Center buttons cluster (left, right, center). - `gamepad.frontButtons`: Front buttons (topLeft, topRight, bottomLeft, bottomRight). #### Gamepad Key Codes If `Shaku.input.delegateGamepadInputToKeys` is set to true (default), the Input manager will generate key-like states for all the connected gamepads that have standard mappings. This means that instead of using the gamepad state object like we demonstrated above, you can query it directly with the main `down()`, `pressed()`, `released()`, `doublePressed()` and `doubleReleased()` methods. The following keys will work supported for every connected gamepad (where X represent the gamepad index starting from 0): - `gamepadX_top`: state of arrow keys top key (left buttons). - `gamepadX_bottom`: state of arrow keys bottom key (left buttons). - `gamepadX_left`: state of arrow keys left key (left buttons). - `gamepadX_right`: state of arrow keys right key (left buttons). - `gamepadX_leftStickUp`: true if left stick points directly up. - `gamepadX_leftStickDown`: true if left stick points directly down. - `gamepadX_leftStickLeft`: true if left stick points directly left. - `gamepadX_leftStickRight`: true if left stick points directly right. - `gamepadX_rightStickUp`: true if right stick points directly up. - `gamepadX_rightStickDown`: true if right stick points directly down. - `gamepadX_rightStickLeft`: true if right stick points directly left. - `gamepadX_rightStickRight`: true if right stick points directly right. - `gamepadX_a`: state of A key (from right buttons). - `gamepadX_b`: state of B key (from right buttons). - `gamepadX_x`: state of X key (from right buttons). - `gamepadX_y`: state of Y key (from right buttons). - `gamepadX_frontTopLeft`: state of the front top-left button. - `gamepadX_frontTopRight`: state of the front top-right button. - `gamepadX_frontBottomLeft`: state of the front bottom-left button. - `gamepadX_frontBottomRight`: state of the front bottom-right button. For example, the following will trigger when the player either press the arrow up key on the gamepad, or move the left stick all the way up: ```js if (Shaku.input.pressed(['gamepad0_up', 'gamepad0_leftStickUp'])) { alert("Move Up!"); } ``` ### Additional Parameters The input manager support some additional parameters you can set: - **preventDefaults**: If true will prevent default input events by calling preventDefault(). - **disableMouseWheelAutomaticScrolling**: If true will disable the special scroll mode that starts in chromium browsers when you click the wheel button not on a link. - **disableContextMenu**: If true will disable the context menu that opens on right mouse button click. - **delegateTouchInputToMouse**: If true will treat touch events (touch start / touch end / touch move) as if the user clicked and moved a mouse. - **delegateGamepadInputToKeys**: If true will generate gamepad key codes so you can query gamepad like you would query a keyboard key. - **resetOnFocusLoss**: If true, will reset input states when the page or target element loses focus. - **defaultDoublePressInterval**: The interval in milliseconds to consider two consecutive key presses as a double-press. ### Conclusion In this chapter we learned about the `Input` manager and how to use it to get input from keyboard, mouse, touch and gamepad. What we covered here is just the common use cases, to learn more about input methods, check out the [online demos](https://ronenness.github.io/Shaku/demo/index.html) or the [online docs](https://ronenness.github.io/Shaku/docs/index.html). ## Assets The *Assets* manager handle loading and runtime creation of game assets, and is accessed by `Shaku.assets`. We already covered most of it while exploring the other managers, but in this section we will focus on the input manager itself. This doc don't cover the entirely of the API, only the main parts of it. To see the full API, check out the [API docs](docs/assets_assets.md). ### Assets Loading There are two things to keep in mind while dealing with the assets manager: #### Assets URL Every asset have a unique identifier field called `url`. This is how we identify the asset and store it in cache. If you try to load the same asset twice, in the second call will just return the copy from the cache, provided the URL is *exactly* the same. When creating assets dynamically by code, you can also provide a unique `url`, if you want to add the asset to cache and make it accessible anywhere via the assets manager. #### Promises Every `load` and `create` method in the assets manager returns a `promise` that is resolved when the asset is loaded. Even if the asset was returned from cache, it will be returned via a promise. Note that you can query the cache directly without loading a new asset, by calling the `getCached()` method. **Promise.asset** All returned promises from the assets manager have additional property: `asset`. This property provide access the asset itself before the promise is resolved, at your own risk (since the asset may be invalid). For example, you can fetch a texture *without waiting* like this: ```js // myTexture may not be loaded yet! let myTexture = Shaku.assets.loadTexture(url).asset; ``` Instead of loading it with `await`: ```js let myTexture = await Shaku.assets.loadTexture(url); ``` Or with `then`: ```js Shaku.assets.loadTexture(url).then((asset) => myTexture = asset); ``` The idea behind the `Promise.asset` p