Quantcast
Channel: Project Drake dev blog » Franca Framework
Viewing all articles
Browse latest Browse all 4

What is Franca Framework?

$
0
0

Recently people have been asking me what the heck I’m working on, how it works, and why I’m working on it. So I figured I’d do a blog post reintroducing the Franca Framework! If you’re into cool tech you might enjoy this post.

Franca Framework is a framework that lets you write code in C#, and lets you build your game either as a managed .NET executable, or a HTML5 browser ready application. Before I talk more about what the framework is, I’m going to tell you what’s in it.

Contents

OpenGL

C#: Using OpenTK
JS: Using WebGL

Franca Framework creates a consistent interface for OpenGL. On the one hand it uses OpenGL 2.1 (because of its similarities to OpenGL ES which WebGL is based on) and on the other hand it uses WebGL. This comes with a SpriteBatch class for 2D drawing, and an Effect class which compiles shaders from plain-text source files, links them into programs (with a default vertex or pixel shader implementation if you want) and then allows you to easily set the uniform variables.

It also comes with a Texture class and SubTexture class which draws only part of a Texture when passed to a SpriteBatch, Color and Vector classes (2D and 3D), Quaternions and Matrices and support for OpenGL buffers. Window creation and management is done via OpenTK or a div element. The window can have various positioning options, including being automatically appended to a named element.

Canvas (system memory surfaces)

C#: Using System.Drawing.Bitmap
JS: Using DOM Canvas element

You might want to manipulate images in system memory before pushing them to OpenGL, whether for direct pixel manipulation or because you want to draw stuff on them. The Canvas class creates a consistent interface for this allowing you to draw on and clear images, even using other images, with things like scaling and alpha-blending. It also lets you get straight at all or a portion of the surface’s pixel data for direct manipulation.

WebSockets

C#: Using asynchronous sockets implementing the WebSocket protocol
JS: Using WebSockets

Does what it says on the tin. Lets you communicate via TCP/IP sockets either from executables or within a browser. On the C# side, because these use a custom implementation of the protocol, all websocket frames don’t need to have their data masked which improves performance a little.

TrueType Fonts

C#: Using OpenType.net (a custom C# port of the library below)
JS: Using OpenType.js

The framework can read TrueType font files into memory and give detailed metrics (including kerning) about glyphs in the font. These glyphs can also be drawn onto the Canvas class mentioned earlier. This is further expanded by a SpriteFont system which dynamically adds glyphs to an OpenGL Texture, allowing TrueType fonts to be rendered in OpenGL with optional edge stroke and gradient. This means drawing text in games is as easy as bundling a font file, and means you don’t need to choose which characters at which font sizes to prerender and bundle with your game. This also improves support for unicode characters.

Keyboard & Mouse Input

C#: Using OpenTK
JS: Using DOM mouse and keyboard events

An InputHandler is available which allows binding key/mouse up, down, repeat while up and repeat while down events to callback functions. Any differences between Javascript and OpenTK are generalized to create a consistent experience across both executable and web platforms. For instance, Javascript cannot distinguish between left and right shift, both return the same code. OpenTK can distinguish between these. The framework lets you bind either left shift, right shift, or the shared shift key. That way a default bind can be set to just shift, which will work on both platforms, but discerning players of the executable version could rebind their keys to left or right shift specifically.

Audio

C#: Using OpenAL and ‘s excellent Ogg streaming class via NVorbis
JS: Using WebAudio API on sensible browsers and Audio element for streaming music, or Audio elements for both music and sound effects on awful browsers like Internet Explorer

Again, does what it says on the tin. Comes with AudioManagers to group music or sounds together and be able to set their volume as a group, as well as master volume controls. For non-WebAudio API compatible browsers this creates a number of HTML audio elements per sound effect (defaults to 2) which it cycles between when the sound plays, this avoids noticeable pauses while the audio element seeks to the start of the sound effect.

Sound effects can be stopped, paused, resumed, have their volume independently changed and be looped and/or pitch shifted. Unfortunately on crap browsers like IE pitch shifting isn’t supported.

JSON Serialization

Using a custom implementation using reflection for both

Yes, Javascript comes with JSON support built-in, but C# cannot dynamically create objects of unspecified type, so I had to create a custom JSON parser. This will parse JSON into nested Dictionary<string,object> instances with List<object> for JSON arrays, or into premade classes, filling similarly named Properties on the objects with JSON data. This will also serialize the other way, serializing objects to JSON either with whitespace or without.

Game & Main Loop

C#: Using Stopwatch and Thread.Sleep while idle
JS: Using requestAnimationFrame, performance.now and Web Workers while idle

Games are driven by the MainLoop class. This class reports whether the GameContainer (where game implementation is located) is currently in focus or running at all. Every frame, or tick, this class calculates the current FPS, processes input events, calls GameContainer.Update with the total running time, calls GameContainer.Step for every step of time elapsed thus far (if using a fixed timestep), then calls GameContainer.Interpolate with the alpha of the remaining time to prevent temporal aliasing, and finally calls Render and SwapBuffers to render the frame.

GameContainer gets all these events necessary for implementing game logic and rendering, and can bind keys and mouse events, generate audio, and render graphics. It can also load game content via its ContentManager. Finally GameContainer can specify whether its framerate should be throttled when idle/not in focus.

This last setting does nothing in HTML5 where browsers always throttle inactive tabs. Unfortunately, browsers enjoy throttling requestAnimationFrame and Javascript timeouts to only 1 frame per second, causing audio and other issues. To prevent this a web worker thread runs in the background which posts event to the main thread, causing it to wake up 5 times per second instead, resolving these kinds of issues.

Files & Content

C#: Using File class
JS: Using XmlHttpRequest

File loading is an issue. In a web environment there is no file system. To emulate this XmlHttpRequest is used, but this is best used in a non-blocking fashion, meaning files load asynchronously and in the background. In C# file loading tends to be a blocking operation. To resolve this discrepancy there’s a static File class which can load data from several types of sources; Canvas (image), binary, text, Buffered Audio and Streaming Audio. On both platforms data is loaded asynchronously, notifying the user through an event or setting a flag on the FileResult which is returned when a file is requested.

Furthermore any class can indicate that they are loadable from one of these 5 base types. This is used to implement the ContentManager which loads game content. This can load effects from JSON (text), Textures (Canvas), SpriteFonts (binary), Sounds (Buffered Audio) and Music (Streaming Audio) just to name a few.

Lua Scripting

C#: Using Luna (a custom Lua interop layer using Lua’s C interop layer) and Lua 5.1
JS: Using lua5.1.js (Lua 5.1 via Emscripten) and Luna

This is still a work in progress, but when done this will allow for attaching Lua scripts to C# objects and easily calling a script’s methods. It also allows for Lua scripts to be passed references to C# objects and to call functions and properties on those objects. Because of the web environment a special import function replaces the usual require keyword found in Lua, allowing Lua to request scripts from the framework by filename. This lets the framework worry about how to load or preload scripts.

This import function also comes with C# style namespaces and a using statement in Lua which allows a Lua namespace to be spread across several files but to interact using one global environment, and to directly access members of both Lua and C# namespaces imported using the using statement. There’s also C# style try/catch/finally support.

Lua scripts can have instance methods, which will be called from C# with a self reference stored outside of the scripts. This allows for live-reloading of scripts while maintaining data for all instances of scripts already running in the game. This means tweaking values on the fly doesn’t require a costly cycle of tweak > compile > run debugger > start application > load content > start game > test new value. It’ll also allow for modding on .NET executables and potentially on web through cross-origin XmlHttpRequests. Because these Lua scripts are properly sandboxed modding becomes safer.

Binary Data & Arrays

C#: Using arrays and BinConverter (an endian aware version of BitConverter)
JS: Using Uint8Array and other typed arrays

Javascript now supports byte arrays for binary data operations, as well as other types of array buffers. To create a unified interface to work with binary data, a few classes had to be created:

BinConverter: C#’s BitConverter is used to convert values to and from byte[] data, but can only perform operations in system endian. BinConverter is an endian-agnostic implementation of BitConverter. This is used to transform values to and from byte data on both platforms. On the web this uses Javascript’s DataView to convert values.

BinaryData: this is basically a byte[] or Uint8Array, with support functions for converting to and from all types of values using BinConverter or copying parts of the array to a new array. This makes loading and creating arbitrarily structured binary data far easier.

TypedArray: These classes (Byte, Double, Float, Int, SByte, Short, UInt and UShort) create a common interface for Javascript style typed arrays, allowing for copying and indexing. For C# these are implemented using arrays and Buffer.BlockCopy.

Miscellaneous

These are some miscellaneous parts of the framework:

Calc: a static class which performs common math operations like Floor, Round, Ceiling, Clamp, Wrap, Max, Min and Lerp (linear interpolation)

Resumable: a coroutines implementation built using IEnumerable’s yield return functionality, allowing functions to be arbitrarily stopped and resumed until done.

RNG: while Javascript comes with a random number generator it cannot be seeded, meanwhile C#’s built-in implementation of Random is confirmed to be buggy (due to a typo in their copied Numerical Recipes code). RNG is a xorshift implementation with consistent output across both platforms and with support for seeds.

UtcDate: C#’s DateTime struct is bad and whoever coded it should feel bad. It’s error prone because it supports both local, utc, and unspecified type dates and times. Javascript meanwhile tracks dates in terms of time elapsed since 1970 while C# tracks time elapsed since year 1. UtcDate is a DateTime struct which makes these internal values present externally in a consistent fashion, as well as prints strings and parses strings for dates in a consistent fashion.

HashSet<T>: Saltarelle Compiler comes with List<T> and Dictionary<K,V> implementations but no hashset, so the Javascript version of the framework contains a reimplementation of .NET’s HashSet<T> class.

How It’s Used

This one is simpler. You create two Visual Studio projects, one using Saltarelle Compiler and one in normal C#, and share your code files between both so long as these files only use the classes and structs found in Franca Framework. SC then compiles this code to functioning Javascript, whereas C# compiles it to a managed code .NET executable. This means that with a single codebase you can target both web and executable platforms.

This Is Insane

Why would you do such a thing? Well, it all started with Ludum Dare. I noticed that unless your game is playable in a browser, most people just won’t play it. I don’t blame them, downloading and unzipping dozens of games is kind of a drag when you can just play the ones that conveniently pop up a browser tab.

So I made a TypeScript framework for making games. Now, I loathe Javascript, and after using it I wasn’t much of a fan of TypeScript either. The bigger problem was that now I was split across two codebases; my C# personal framework which was extensive and had lots of cool stuff so I could do things easily, and my TS framework which was way less cool and forever neglected because working on it felt like a waste of time when I already had my C# framework.

This meant that even if a Ludum Dare game I made had an interesting concept I couldn’t be bothered polishing it up. Working on it implied being hobbled by a comparatively incomplete code library and working in a language I didn’t enjoy. Furthermore, I couldn’t convert Ludum Dare games into standalone games easily without completely redoing them in C#.

I started Franca Framework as a hobby/personal challenge to see if I could fix that. If I could have just a single, unified Framework that would let me code in a language I enjoy (C#) and would let me reuse most of my good code with little effort (also C#) but also let me target web I’d be in good shape. The added benefit would be that C#’s debugging facilities are far superior to those found even in modern browsers like Chrome and Firefox.

Just by tinkering with it the framework kind of got rather enormous, though obviously a lot of these systems still require some work here and there. Still, it’s usable already, and I could be making games in this. In fact I would be, but I’m still working on MidBoss first. After that I’ll make the transition to the new framework, and you can expect web based demos for my games.

And Then Scripting

The idea of running Lua scripts came when I got tired of tweaking on-screen healthbars in MidBoss to be one pixel up or down for half an hour. All I was doing was changing a value then waiting. I realized I could increase productivity if I could just reload parts of my code while the game kept running, and figured Lua would be perfect for that and modding. Fortunately I found lua5.1.js which makes it possible to use Lua within a browser and which crucially also lets you interface with it through Lua’s C interoperability layer.

This means I could add Lua support through the interop layer in C# and have it work more or less the same regardless of whether it’s running on .NET or in JS. So far experiments are going well, and I think I’ll be able to pull it off!

Example

Finally for those interested in seeing what a simple Franca Framework program would look like, here’s some sample code.

Program.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Franca;

namespace CoreTest {
    class Program {
        static void Main() {
            FrancaFramework.Init(FrancaRun);
        }
    
        static void FrancaRun() {
            // parent element is <div id="main">
            var win = new Container("main", 640, 480);
            win.Run();
        }
    }
}

Container.cs

using Franca;
using Franca.Audio;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace CoreTest {
    public class Container : GameContainer {
        private SpriteBatch batch;
        private Texture ball;
        private Effect effect;

        private SpriteFont spriteFont;

        private AudioManager soundManager;
        private Sound testSound;
        private AudioManager musicManager;
        private Music testMusic;

        private ClientSocket socket;

        public Container(string anchor, int width, int height)
            : base(anchor, width, height) {
            VSync = false;
        }

        public override void LoadContent() {
            base.LoadContent();

            soundManager = new AudioManager(AudioContext);
            musicManager = new AudioManager(AudioContext);
            
            // set the default font manager to have a 1024x1024 texture 
            GraphicsDevice.DefaultFontManager = new SpriteFontManager(
                GraphicsDevice, 1024, 1024);

            ball = Content.LoadTexture("ball.png");
            effect = Content.Load<Effect>("effect.json");
            spriteFont = Content.LoadFont("Roboto-Black.ttf");
            testSound = Content.LoadSound("boom", soundManager);
            testMusic = Content.LoadMusic("tune", musicManager);

            batch = new SpriteBatch(GraphicsDevice);
        }

        public override void Update(TimeSpan gameTime, TimeSpan elapsed) {
            // process input binds
            Input.ProcessInput(gameTime);
            base.Update(gameTime, elapsed);
        }

        public override void Render(TimeSpan gameTime, TimeSpan elapsed) {
            GraphicsDevice.Clear(Color.CornflowerBlue, ClearOptions.All);

            if (!Content.LoadingContent) {
                batch.Begin(BatchSortMode.Texture);
                batch.Draw(ball, new Vector2(250, 250), Color.Red);
                batch.End();
            }

            if (spriteFont.Ready) {
                // write "Hello world!" with a white-gray color gradient, 
                // and a gray-black gradient stroke
                spriteFont.Draw("Hello world!", 16, new Vector2(50, 50), 
                    Color.White, Color.Gray, // glyph gradient
                    true, Color.Gray, Color.Black); // stroke and gradient
                
                // write the frames per second in white with a black stroke
                spriteFont.Draw(FPS, 12, new Vector2(10, Height - 10), 
                    Color.White, // glyph color
                    true, Color.Black); // stroke and color
            }

            base.Render(gameTime, elapsed);
        }

        public override void BindKeys() {
            base.BindKeys();

            // writes button name, event type and mouse X,Y  
            // coordinates on mouse down and up events
            Input.Bind(MouseButtons.Left, InputType.Down, 
                (e) => { 
                    Console.WriteLine(
                        e.MouseBind.Value.ButtonType.GetName<MouseButtons>() + 
                        ", " + e.MouseBind.Value.Type.GetName<InputType>() + 
                        " (" + e.Mouse.X + ", " + e.Mouse.Y + ")"); 
                }
            );
            Input.Bind(MouseButtons.Left, InputType.Up, 
                (e) => { 
                    Console.WriteLine(
                        e.MouseBind.Value.ButtonType.GetName<MouseButtons>() + 
                        ", " + e.MouseBind.Value.Type.GetName<InputType>() + 
                        " (" + e.Mouse.X + ", " + e.Mouse.Y + ")");
                }
            );

            // play sound and music
            Input.Bind(Key.F, InputType.Down, 
                (e) => {
                    if (testSound.Loaded)
                        testSound.Play();
                }
            );
            Input.Bind(Key.M, InputType.Down, 
                (e) => {
                    if (testMusic.Loaded) {
                        if (testMusic.State == AudioState.Stopped)
                            testMusic.Play();
                        else
                            testMusic.Stop();
                    } 
                }
            );

            // connect to WebSocket server
            Input.Bind(Key.C, InputType.Down, 
                (e) => {
                    if (socket != null && socket.State != SocketState.Closed) {
                        socket.Close();
                        socket = null;
                        return;
                    }
                    if (socket == null) {
                        socket = new ClientSocket();
                        socket.OnClose += socket_OnClose;
                        socket.OnConnect += socket_OnConnect;
                        socket.OnError += socket_OnError;
                        socket.OnMessage += socket_OnMessage;
                    }
                    socket.Connect("localhost", 8000);
                }
            );
        }

        void socket_OnMessage(ClientSocket socket, ClientMessage message) {
            Console.WriteLine("Socket message: " + message.Data);
        }

        void socket_OnError(ClientSocket socket, Exception error) {
            Console.WriteLine("Socket error: " + error.Message);
        }

        void socket_OnConnect(ClientSocket socket) {
            Console.WriteLine("Socket connected");
            socket.Send("Hello world");
        }

        void socket_OnClose(ClientSocket socket, CloseCode code, string reason) {
            // Socket closed: <reason> (<codenum>, <codename>)
            Console.WriteLine("Socket closed:" + 
                (!reason.IsNullOrEmpty() ? " " + reason + " (" : " (") + 
                (int)code + ", " + code.GetName<CloseCode>() + ")");
        }

        public override void Exit() {
            batch.Dispose();
            base.Exit();
        }
    }
}

Viewing all articles
Browse latest Browse all 4

Trending Articles