16 releases

✓ Uses Rust 2018 edition

new 0.3.14 Jul 14, 2019
0.3.13 Jul 12, 2019
0.3.12 Jun 29, 2019
0.3.9 May 13, 2019
0.1.0 Apr 19, 2019
Download history 9/week @ 2019-04-14 34/week @ 2019-04-21 71/week @ 2019-04-28 39/week @ 2019-05-05 42/week @ 2019-05-12 47/week @ 2019-05-19 28/week @ 2019-05-26 26/week @ 2019-06-02 37/week @ 2019-06-09 35/week @ 2019-06-16 23/week @ 2019-06-23 134/week @ 2019-06-30 69/week @ 2019-07-07

199 downloads per month

MIT license

156KB
4K SLoC

logo

Oxygengine

The hottest HTML5 + WASM game engine for games written in Rust with web-sys.

Table of contents

  1. Installation
  2. Project Setup
  3. Building for development and production
  4. Roadmap
  5. Hello World

Installation

  1. Make sure that you have latest node.js with npm tools installed (https://nodejs.org/)
  2. Make sure that you have latest wasm-pack toolset installed (https://rustwasm.github.io/wasm-pack/installer/)
  3. Make sure that you have latest create-rust-webpack nodejs package installed (npm install -g create-rust-webpack)

Project Setup

Create Rust + WASM project with create-rust-webpack:

create-rust-webpack <path>

where path is path to empty folder where your project will be created by this command.

Then add this record into your /Cargo.toml file:

[dependencies]
oxygengine = { version = "0.3", features = ["web-composite-game"] }

where web-composite-game means that you want to use those modules of Oxygen game engine, that gives you all features needed to easly make an HTML5 web game with composite renderer. You may also select which exact features you need, excluding those which you're not gonna use. For example: web-composite-game feature by default enables these features: composite-renderer, input, network. So if you just want to make a movie-like animation then you don't need any input or networking, so you will want to add this record instead:

[dependencies.oxygengine]
version = "0.3"
features = [
    "web",
    "composite-renderer",
    "oxygengine-composite-renderer-backend-web"
]

which means you want to use composite renderer with web backend and produce app for web target.

Building for development and production

  • Launch live development with hot reloading (app will be automatically recompiled in background):
npm start
  • Build production distribution (will be available in /dist folder):
npm run build
  • Build crate without of running dev env:
cargo build --all --target wasm32-unknown-unknown

or build with default target set to wasm32-unknown-unknown:

cargo build --all

with /.cargo/config file:

[build]
target = "wasm32-unknown-unknown"

TODO / Roadmap

  • UI widgets
  • Prefabs (loading scenes from asset)
  • Packed assets fetch engine
  • Hardware renderer
  • WebGL hardware renderer backend
  • 2D physics

Hello World

/webpack.config.js

const path = require("path");
const CopyPlugin = require("copy-webpack-plugin");
const WasmPackPlugin = require("@wasm-tool/wasm-pack-plugin");

const dist = path.resolve(__dirname, "dist");
const DEBUG = true;
console.log('BUILD MODE: ' + (DEBUG ? 'DEBUG' : 'RELEASE'));

module.exports = {
  mode: DEBUG ? 'development' : 'production',
  entry: {
    index: "./js/index.js"
  },
  output: {
    path: dist,
    filename: "[name].js"
  },
  devServer: {
    contentBase: dist,
  },
  plugins: [
    new CopyPlugin([
      path.resolve(__dirname, "static")
    ]),

    new WasmPackPlugin({
      crateDirectory: __dirname,
      extraArgs: "--out-name index",
      forceMode: DEBUG ? undefined : 'release',
    }),
  ]
};

/static/index.html:

<!DOCTYPE html>
<html>
  <head>
    <meta http-equiv="Content-type" content="text/html; charset=utf-8"/>
    <title>Oxygen Engine Game</title>
  </head>
  <body style="margin: 0; padding: 0;">
    <canvas
      id="screen"
      style="margin: 0; padding: 0; position: absolute; width: 100%; height: 100%;"
    ></canvas>
    <script src="index.js"></script>
  </body>
</html>

where screen canvas is our target fullpage game screen where game will be rendered onto.

/src/lib.rs:

use oxygengine::prelude::*;
use wasm_bindgen::prelude::*;

#[cfg(feature = "wee_alloc")]
#[global_allocator]
static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;

// component that tags entity as moved with keyboard.
#[derive(Debug, Default, Copy, Clone)]
pub struct KeyboardMovementTag;

impl Component for KeyboardMovementTag {
    // tag components are empty so they use `NullStorage`.
    type Storage = NullStorage<Self>;
}

// component that tells the speed of entity.
#[derive(Debug, Default, Copy, Clone)]
pub struct Speed(pub Scalar);

impl Component for Speed {
    // not all entities has speed so we use `VecStorage`.
    type Storage = VecStorage<Self>;
}

// system that moves tagged entities.
pub struct KeyboardMovementSystem;

impl<'s> System<'s> for KeyboardMovementSystem {
    type SystemData = (
        // we will read input.
        Read<'s, InputController>,
        // we will read delta time from app lifecycle.
        ReadExpect<'s, AppLifeCycle>,
        // we will read speed components.
        ReadStorage<'s, Speed>,
        // we will filter by tag.
        ReadStorage<'s, KeyboardMovementTag>,
        // we will write to transforms.
        WriteStorage<'s, CompositeTransform>,
    );

    fn run(
        &mut self,
        (input, lifecycle, speed, keyboard_movement, mut transforms): Self::SystemData,
    ) {
        let dt = lifecycle.delta_time_seconds() as Scalar;
        let hor = -input.axis_or_default("move-left") + input.axis_or_default("move-right");
        let ver = -input.axis_or_default("move-up") + input.axis_or_default("move-down");
        let offset = Vec2::new(hor, ver);

        for (_, speed, transform) in (&keyboard_movement, &speed, &mut transforms).join() {
            transform.set_translation(transform.get_translation() + offset * speed.0 * dt);
        }
    }
}

pub struct GameState;

impl State for GameState {
    fn on_enter(&mut self, world: &mut World) {
        // create entity with camera to view scene.
        world
            .create_entity()
            .with(CompositeCamera::new(CompositeScalingMode::CenterAspect))
            .with(CompositeTransform::scale(400.0.into()))
            .build();

        // create player entity.
        let player = world
            .create_entity()
            .with(CompositeRenderable(
                Rectangle {
                    color: Color::red(),
                    rect: [-50.0, -50.0, 100.0, 100.0].into(),
                }
                .into(),
            ))
            .with(CompositeTransform::default())
            .with(KeyboardMovementTag)
            .with(Speed(100.0))
            .build();

        // create eye attached to player.
        world
            .create_entity()
            .with(CompositeRenderable(
                Rectangle {
                    color: Color::yellow(),
                    rect: [-10.0, -10.0, 20.0, 20.0].into(),
                }
                .into(),
            ))
            .with(CompositeTransform::translation((-20.0).into()))
            .with(Parent(player))
            .build();

        // create hint text.
        world
            .create_entity()
            .with(CompositeRenderable(
                Text {
                    color: Color::white(),
                    font: "Verdana".into(),
                    align: TextAlign::Center,
                    text: "Use WSAD to move".into(),
                    position: 0.0.into(),
                    size: 24.0,
                }
                .into(),
            ))
            .with(CompositeTransform::translation([0.0, 100.0].into()))
            .with(CompositeRenderDepth(-1.0))
            .build();
    }
}

#[wasm_bindgen(start)]
pub fn main_js() -> Result<(), JsValue> {
    #[cfg(debug_assertions)]
    console_error_panic_hook::set_once();

    // Application build phase - install all systems and resources and setup them.
    let app = App::build()
        // install core module assets managment.
        .with_bundle(
            oxygengine::core::assets::bundle_installer,
            (WebFetchEngine::default(), |assets| {
                // register assets protocols from composite renderer module.
                oxygengine::composite_renderer::protocols_installer(assets);
            }),
        )
        // install input managment.
        .with_bundle(oxygengine::input::bundle_installer, |input| {
            // register input devices.
            input.register(WebKeyboardInputDevice::new(get_event_target_document()));
            // input.register(WebMouseInputDevice::new(get_event_target_by_id("screen")));
            // map input axes and triggers to devices.
            input.map_axis("move-up", "keyboard", "KeyW");
            input.map_axis("move-down", "keyboard", "KeyS");
            input.map_axis("move-left", "keyboard", "KeyA");
            input.map_axis("move-right", "keyboard", "KeyD");
            // input.map_axis("mouse-x", "mouse", "x");
            // input.map_axis("mouse-y", "mouse", "y");
            // input.map_trigger("mouse-left", "mouse", "left");
        })
        // install composite renderer.
        .with_bundle(
            oxygengine::composite_renderer::bundle_installer,
            WebCompositeRenderer::with_state(
                get_canvas_by_id("screen"), // canvas target.
                RenderState::new(Some(Color::black())),
            ),
        )
        .with_system(KeyboardMovementSystem, "keyboard_movement", &[])
        .build(GameState, WebAppTimer::default());

    // Application run phase - spawn runner that ticks our app.
    AppRunner::new(app).run(WebAppRunner)?;

    Ok(())
}

Dependencies

~6.5MB
~115K SLoC