• Announcements

    • khawk

      Download the Game Design and Indie Game Marketing Freebook   07/19/17

      GameDev.net and CRC Press have teamed up to bring a free ebook of content curated from top titles published by CRC Press. The freebook, Practices of Game Design & Indie Game Marketing, includes chapters from The Art of Game Design: A Book of Lenses, A Practical Guide to Indie Game Marketing, and An Architectural Approach to Level Design. The GameDev.net FreeBook is relevant to game designers, developers, and those interested in learning more about the challenges in game development. We know game development can be a tough discipline and business, so we picked several chapters from CRC Press titles that we thought would be of interest to you, the GameDev.net audience, in your journey to design, develop, and market your next game. The free ebook is available through CRC Press by clicking here. The Curated Books The Art of Game Design: A Book of Lenses, Second Edition, by Jesse Schell Presents 100+ sets of questions, or different lenses, for viewing a game’s design, encompassing diverse fields such as psychology, architecture, music, film, software engineering, theme park design, mathematics, anthropology, and more. Written by one of the world's top game designers, this book describes the deepest and most fundamental principles of game design, demonstrating how tactics used in board, card, and athletic games also work in video games. It provides practical instruction on creating world-class games that will be played again and again. View it here. A Practical Guide to Indie Game Marketing, by Joel Dreskin Marketing is an essential but too frequently overlooked or minimized component of the release plan for indie games. A Practical Guide to Indie Game Marketing provides you with the tools needed to build visibility and sell your indie games. With special focus on those developers with small budgets and limited staff and resources, this book is packed with tangible recommendations and techniques that you can put to use immediately. As a seasoned professional of the indie game arena, author Joel Dreskin gives you insight into practical, real-world experiences of marketing numerous successful games and also provides stories of the failures. View it here. An Architectural Approach to Level Design This is one of the first books to integrate architectural and spatial design theory with the field of level design. The book presents architectural techniques and theories for level designers to use in their own work. It connects architecture and level design in different ways that address the practical elements of how designers construct space and the experiential elements of how and why humans interact with this space. Throughout the text, readers learn skills for spatial layout, evoking emotion through gamespaces, and creating better levels through architectural theory. View it here. Learn more and download the ebook by clicking here. Did you know? GameDev.net and CRC Press also recently teamed up to bring GDNet+ Members up to a 20% discount on all CRC Press books. Learn more about this and other benefits here.
  • entries
    15
  • comments
    32
  • views
    28060

About this blog

Toasted mystery box

Entries in this blog

fastcall22

Selective quote

Oops, four months just went by in a blink. I hear it gets worse when you get older. Scary.

Anyway, here's a CSS rule to keep the new selective quote modal at the bottom right corner of the screen. I like to select-along while I read, and this pesky modal is always in my way:
[code=css:0]@namespace url(http://www.w3.org/1999/xhtml);@-moz-document domain("www.gamedev.net") {#ddk33_qpopup_popup { position: fixed !important; left: unset !important; top: unset !important; right: 0px !important; bottom: 0px !important;}}And a screen shot:
Zkbg3Xw.png


That's all for now.

fastcall22

Waiting for Visual Studio to update, so I thought I'd write something.

When working with streams such as file or audio processing, you usually run into the same problem: How to copy the most data without knowing how large the stream of data is. I solved a similar problem at work and in my audio synthesizer. In the case of the synthesizer, I wanted an echo effect that fed back N stored seconds of audio.

Here's how a simple block transfer could look:
// assuming:HANDLE src;HANDLE dst;int src_size;// and:const int block_size = 8 * 1000; // 8 KBchar* block = alloc(block_size);// then:int remaining = src_size;for ( int bytes_read = 0; bytes_read = read(src, block, block_size)); remaining -= bytes_read ){ // (process block) write(dst, block, bytes_read); remaining -= bytes_read;}For a block_size of 100 and a src_size of 550, you would make 6 writes of sizes:


100 100 100 100 100 50

The echo effect was a bit different: I have two buffers of known length, but one of the buffers is circular and has a read and write head at two different positions within that buffer.
// assuming:int samples = 44100;int audio_buffer_len = samples / 100;float* out_audio = new float[audio_buffer_len]; // 1-channel audio at 44.1KHz, 100ms buffer// and some state:struct delay_effect { float delay_amp = 0.25f; int delay_stride = samples * 2 / 3; // 667ms or 29400 samples int buffer_size = delay_stride + 320; // some extra padding float* buffer = new float[buffer_size]; int wdx = 0; // write position within buffer int rdx = delay_stride; // read position within buffer // the extra padding on buffer is needed to keep the write head from interfering // with the read head and vice-versa void apply(float* out_audio, int audio_buffer_len);};// then:void delay_effect::apply(float* out_audio, int audio_buffer_len) { float* out_ap = out_audio; int samples_remaining = audio_buffer_len; while ( samples_remaining ) { // find minimum number of samples needed such that: // 1. read head does not overflow buffer // 2. write head does not overflow buffer // 3. audio buffer is not overflowed int take = min( min(buffer_size - wdx, buffer_size - rdx), samples_remaining ); // apply effect to out_audio // (SIMD candidate: snap to 16-byte boundaries and lengths, use float4s instead) float* in_bp = buffer + rdx; float* out_bp = buffer + wdx; for ( int idx = 0; idx < take; ++idx ) { // read from buffer, apply damping, accumulate into output audio, // and store the mixed audio into y float y = (*out_ap++ += *in_bp++ * damping); // feed audio back into buffer *out_bp++ = y; } // advance read and write position, wrapping around buffer length wdx = (wdx + take) % buffer_size; rdx = (rdx + take) % buffer_size; samples_remaining -= take; }}Stepping through, you might see something like this for an audio buffer sized 50, and an echo buffer sized 190, and a delay of 85 samples:
buf# loop# take rdx wdx 1 1 50 0 85 2 1 50 50 135 3 1 5 100 185 3 2 45 105 0 4 1 40 150 45 4 2 10 0 85 5 1 50 10 95 6 1 45 60 145 6 2 5 105 0 7 1 50 110 5As you can see, the entire audio buffer is covered: sum(`take`) group by `buf#`
And neither rdx+take nor wdx+take extends past the echo buffer's 190 samples.


Okay, that's all for now.
See you around.

fastcall22

Maximized IPBoard Chat

Hi there. It's been a while. Spent some time today fixing up the stupid IPBoard chat. I somehow managed to monkey-patch IPBoard's styles to maximize use of screen space:

Before:
xzmlbsw.png

After:
F4UgxdS.png
70KqX4X.png
RxzwX7Y.png

The style can be found below and can be used installed using the Stylish addon (Firefox/Chrome). Chrome users must remove the @-moz-document directive and surrounding braces.

@namespace url(http://www.w3.org/1999/xhtml);@-moz-document url("http://www.gamedev.net/chat/") {* { box-sizing: border-box !important; }body { padding: 0 !important;}html, body { height: 100%; width: 100%; overflow: hidden;}#ipbwrapper { max-width: initial; max-height: initial; height: 100%; height: 100%;}#chatters-online-wrap,#scrollbar_container,.ipsLayout_content,#ipbwrapper > .ipsLayout { height: 100%;}.ipsLayout_content,#ipbwrapper > .ipsLayout > .ipsLayout_right { height: 100%; float: right; display: table;}.ipsLayout_content > *,#ipbwrapper > .ipsLayout > .ipsLayout_right > * { display: table-row;}#chatters-online,#messages-display { position: absolute; left: 0px; top: 0x; width: 100%; height: 100%;} @media (max-width: 960px) { .ipsBox_container.ipsLayout_right { width: 250px !important; float: right !important; } .ipsBox_container.ipsLayout_content { overflow: hidden !important; height: 100% !important; width: initial !important; }}#chatters-online { position: absolute; width: 100%; height: 100%; overflow: scroll; left: 0px; top: 0px;}#chatters-online-wrap { overflow: hidden; position: relative; left: 0px; top: 0px;}#chatters-online-wrap + .ipsPad.right { float: initial; text-align: right;}}Anywho, that's all for now. See you around.


EDIT:
Quick fixed chatrooms, small screen sizes.

fastcall22

Hi there.

Someone on the chat today was asking about the pimpl idiom, and I thought I'd write up a quick little example. The purpose of an opaque pointer is to move the the implementation specific dependencies (types, macros, includes, and et. al.) away from the header and into the actual source where its used. Including "Foobar.h" from "Unrelated.cpp" also includes iostream, string, vector, windows.h (namespace polluting!), and the entirety of "expensive third party library". If "Foobar" used an opaque pointer, then "Unrelated.cpp" can use a Foobar, without having to know about "string", "vector", "windows.h", or the third party library.

Let's say, as a contrived example, you wanted to wrap std::vector. Such a class might look something like:


// simple_list.h#include #include class simple_list {public: void add(const std::string& str); void dump() const;private: std::vector _data;};// simple_list.cpp#include "simple_list.h"void simple_list::add(const std::string& str) { _data.push_back(str);}Notice how including "simple_list.h" will also include "vector" and "string".


An opaque pointer will hide the details of its implementation:
// simple_list.hclass simple_list {public: simple_list(); ~simple_list(); simple_list(const simple_list&) = delete; simple_list& operator = ( const simple_list& ) = delete;public: void add(const char* str); void dump() const;private: struct impl; impl* _impl;};// simple_list.cpp#include "simple_list.h"#include #include struct simple_list::impl { std::vector data;};simple_list::simple_list() { _impl = new impl;}simple_list::~simple_list() { delete impl;}simple_list::add(const char* str) { _impl->data->push_back(str);}Note how simple_list.h no longer requires the "vector" or "string" headers, and how the implementation structure is well contained in simple_list.cpp. Unrelated.cpp can use a "simple_list" without first knowing about "vector" or "string".


Also note how simple_list now has a dynamic allocation with it. This is one of the main disadvantages of an opaque pointer, but it can be avoided.

This is my favorite approach to the opaque pointer idiom: Just reserve some space for the implementation:
// simple_list.h#pragma onceclass simple_list {public: // noncopyable simple_list(); ~simple_list(); simple_list(const simple_list&) = delete; simple_list& operator= ( const simple_list& ) = delete;public: // interface void add(const char* str); void dump() const;private: // pimpl struct internal_impl; internal_impl& impl(); const internal_impl& impl() const;private: // data int _data[6];};// simple_list.cpp#include "simple_list.h"#include #include #include #include struct simple_list::internal_impl { internal_impl() {} ~internal_impl() {} std::vector _data;};simple_list::simple_list() { static_assert(sizeof(internal_impl) < sizeof(_data), "simple_list::_data too small for internal_impl"); new (_data) internal_impl;}simple_list::~simple_list() { impl().~internal_impl();}void simple_list::add(const char* str) { impl()._data.push_back(str);}void simple_list::dump() const { using namespace std; const auto& vec = impl()._data; cout << '{'; if ( vec.size() ) { copy(vec.begin(), vec.end()-1, ostream_iterator(cout, ", ")); cout << vec.back(); } cout << '}';}simple_list::internal_impl& simple_list::impl() { return *reinterpret_cast(_data);}const simple_list::internal_impl& simple_list::impl() const { return *reinterpret_cast(_data);}// main.cpp#include "simple_list.h"int main(int, char*[]) { simple_list f; f.add("the"); f.add("quick"); f.add("brown"); f.add("fox"); f.dump();}The simple_list now reserves 6*sizeof(int) bytes for the implementation, backed by a static_assert on the constructor to make sure the buffer doesn't overrun. Memory alignment and cache lines considered, this might be an acceptable compromise: Trade off the double indirection with a possibility of some wasted space.


Though, the point is moot, because in this contrived example, the vector and all its strings will store its data on the heap anyways.

That's all for now. See you around.

fastcall22

Learning Blender

Hi there! Long time no see.

Been busy, but learning Blender. I figured if I knew my way around various GNU programs (GNU screen, less, vim) and their keyboard shortcuts, how bad could Blender be? I've spent the past few days learning blender, and I am loving it! Though, compared to 3dsmax, the object modifier stack is a bit limiting. I originally set out to make a configurable flipper, such that I could adjust the detail as needed; number of sides to each round side of the flipper, bevel depth, and etc. Unfortunately, blender does not have a "select volume operator", nor could I adjust the number of sides of a cylinder after creating it. Oh well, I guess I'll have to make do-- make a template "flipper", which retains as many operators as possible, then make a duplicate, apply operators, and work with the clone.

Anyway, here's what I've been working on. The specular on the ground plane is a bit ridiculous.
XbmDePH.png

In the long-term future, I'm planning on trying out the blender game engine and perhaps get a pinball game going. Depends on a lot of things.

Okay, bye.

fastcall22

Pongout 0.83 WIP

Well, it's about time I posted some progress pics of the game I've been working on the past month or so. smile.png

Pongout is a crossover of breakout and pong, written in moonscript. It originally started as a test to see how Box2D will behave in a breakout game, and then suddenly the rest of this happened. This week was spent bug fixing, polishing, and reworking the third level. Those who have been following my earlier alpha and beta versions in the chat will be delighted to know that the game now runs in borderless fullscreen.

The game currently doesn't have any in-game menus or in-game instructions, so I've included the instructions below.

Download


Platform-independent: pongout_0.83.love (0.11 MB), requires LOVE
Prepackaged Windows x64: pongout_0.83_win_x64.zip (3.63 MB)


Feedback is appreciated. smile.png
There is an odd issue on some machines (read: my machines) that causes the game to be extra choppy unless an application that uses a high-performance timer is running in the background. If you run into this issue as well, please let me know.


Controls


Mouse Controls paddlesLeft mouse Launches ballsleft/right Apply torquer Resetescape QuitDebug controls: (forfeits highscore)] Hit all bricks oncen Next level?

Objective


The goal of the game is to score as many points as possible by skillfully using the ball and paddle to destroy all bricks on the playing field.


legend.png

Legend


(A) Ball
(B) Brick
(C?) Paddle
(D) Unbreakable brick
(E) Ghost
(F) Score earned and score earned from global multiplier
(G) Lives remaining
(H) Current score
(I) Global multiplier
(J) High score


Ball


A ball accumulates multiplier by hitting normal bricks, up to 10x multiplier. As it does so, its speed will increase up to 50%. Hitting a paddle will reset the ball's multiplier and speed.

Use torque to get your ball unstuck or skillfully launch your ball back into the playing field. Use with discretion, as each use applies an increased speed penalty.


Paddle


Can redirect balls depending on where on the paddle they hit. Use this to aim the ball in the right direction.


Brick


Hit these for points and multiplier. Unbreakable bricks are worth less and do not increment multiplier.

Once all the breakable bricks are destroyed, the field is cleared and a new level will begin.


Ghost


Occupies breakable bricks. Hitting a brick inhabited by a ghost will grant a small amount of global multiplier and the ghost will attempt to locate to another vacant brick.

If there are no remaining bricks to occupy or if all breakable bricks are occupied with ghosts, then the ghosts will begin to panic and bonus time begins.


Bonus Time


Panicked ghosts will consume their host bricks over a period of time. Destroy them quickly before the level ends to get up to 1500 bonus points each.


Scoring


There are four parts to scoring: Base score, multiplier, flat score, and global multiplier. Base score is affected by multiplier whereas flat score is not, and both are affected by global multiplier:

score += global*(flat + base*multiplier)

Normal brick: 10 base and 1 multiplier
Unbreakable brick: 1 base
Ghost: 1/16% global multiplier and/or between 100 and 1500 flat during bonus time.


Screenshots


Level 1
level-1.png

Level 3 rework
level-3.png
fastcall22

Changelog:
Added new emotes.
Fixed literal backslash escaping.
Changed users command to link users to their profiles.
And various other things since version 1.0 that I've forgotten.

// ==UserScript==// @name IPBoard Extensions// @namespace fastcall22.com// @include http://www.gamedev.net/chat/*// @version 1.5// @grant none// ==/UserScript==(function ipb_ext(){try{ var Chat = IPBoard.prototype.chat; // util Chat.ext_post_message = function(content_or_nodes,content_is_html) { var post = document.createElement('li'); ['post','chat-myown'].forEach(function(cl){ post.classList.add(cl); }); if ( typeof content_or_nodes == 'string' ) { var content = content_or_nodes; var target = content_is_html ? 'innerHTML' : 'textContent'; post[target] = content; } else { var nodes = content_or_nodes; [].concat(nodes).forEach(function(elem){ post.appendChild(elem); }); } document.getElementById('storage_chatroom') .appendChild(post); post.scrollIntoView(false); }; Chat.ext_search_users = function(query) { var result = {}; var q_reg = new RegExp(query||'^'); for ( var id in Chat.forumIdMap._object ) { var name = Chat.nameFormatting._object[id][2]; if ( q_reg.test(name) ) result[name] = id; } return result; }; Chat.ext_user_action = function(action,target_id){ ipb.chat.sendMessageToChild( "server="+serverHost + "&path="+serverPath + "&room="+roomId + "&user="+userId + "&access_key=" + accessKey + "&action="+action + "&against="+target_id ); ipb.chat.lastAction = parseInt(new Date().getTime().toString().substring(0, 10)); }; // config var null_f = function(){}; Chat.ext_empty_event = { stopPropagation: null_f, preventDefault: null_f }; Chat.ext_confirm_action = null; var make_action = function(action){ return function(query) { if ( !query.trim() ) return; var users = Chat.ext_search_users(query); Chat.ext_confirm_action = function() { for ( var k in users ) Chat.ext_user_action(action,users[k]); }; var str = ''; for ( var k in users ) { if ( str ) str += ', '; str += k; } Chat.ext_post_message("Send /confirm to "+action+": "+str+". Send /cancel to ignore"); }; }; Chat.ext_commands = { kick: make_action('kick'), ban: make_action('ban'), test: function() {Chat.test = !(Chat.test || false)}, debug: function() {Chat.debug = !(Chat.debug || false)}, users: function(query) { var users = {}; var keys = []; [].slice.call(document.querySelectorAll('#chatters-online li')) .forEach(function(li){ var a = li.querySelector('a.ipsUserPhotoLink'); var user_id = li.getAttribute('id').replace(/^user_/,''); var user_profile = a.getAttribute('href'); var user_name = Chat.nameFormatting._object[user_id][2]; var out_a = document.createElement('a'); out_a.setAttribute('href',user_profile); out_a.textContent = user_name; users[user_name] = out_a; keys.push(user_name); }); var nodes = []; keys.sort().forEach(function(key) { if ( nodes.length ) nodes.push(document.createTextNode(', ')); nodes.push(users[key]); }); Chat.ext_post_message(nodes); }, confirm: function() { if ( !Chat.ext_confirm_action ) return; Chat.ext_confirm_action(); Chat.ext_confirm_action = null; }, cancel: function() { Chat.ext_confirm_action = null; }, mute: function() { document.getElementById('sound_toggle').click(); }, quit: function(){ document.getElementById('leave_room').click(); }, }; Chat.ext_escape = { 'n': '\n', 't': '?', 'e': '?', '\\': '\\', }; Chat.ext_emotes = { fear: ':f34r:', lenny: '( ?? ?? ??)', wat: '?__?', shrug: '?\\_(?)_/?', whee: '?( ? )?', flip: '(????)??????', why: '?(????)', }; Chat.ext_bbc = { rick: function(param,content) { // never gonna give you up // never gonna let you down // never gonna run around // and desert you } }; // helpers Chat.arg_parse = /\s*(?:(\w+?)(?:="(.+?)"))?/g; Chat.bbc_filter = function ext_bbc_filter(_,tag,param,content) { var f = Chat.ext_bbc[tag]; if ( !f ) return _; return f(param,content); }; Chat.ext_filters = [ /* commands */ [ /^\/(\w+)\s*(.*)$/, function ext_command_filter(_,cmd,argstr) { var args = argstr.split(' '); var f = Chat.ext_commands[cmd]; if ( !f ) return _; f.apply(null,args); return ''; } ], /* emotes */ [ /:(\w+):/g, function ext_emote_filter(_,tag) { return Chat.ext_emotes[tag] || _; } ], /* backslash */ [ /\\(?:(x[0-9a-fA-f]{4})|([a-z\\]))/g, function ext_backslash_filter(_,hex,code) { if ( hex ) return '&#'+hex+';'; return Chat.ext_escape || _; } ], /* BBC w/ content */ [ /\[(\w+)(?:=(.+))?\](.*?)\[\/\1\]/g, Chat.bbc_filter ], /* self-closing BBC tags */ [ /\[(\w+)(?:=(.+))?\s*()\/\]/g, Chat.bbc_filter ], /* general */ [ /\t/g, '?' ] ]; Chat.ext_filter_message = function(str) { Chat.ext_filters.forEach(function(f){ if ( !str.length ) return; if ( Chat.debug ) console.log(f,str); str = String.prototype.replace.apply(str,f); }); return str; }; Chat.ext_send = function ext_send(e) { if ( e.preventDefault ) e.preventDefault(); if ( e.stopPropagation ) e.stopPropagation(); var message = (document.getElementById('message_textarea').value || '').trim(); if ( Chat.debug ) console.log('send',message); if ( !message ) return; var filtered = Chat.ext_filter_message(message) || null; document.getElementById('message_textarea').value = filtered; if ( filtered ) return Chat.ipb_sendChat.call(this,Chat.ext_empty_event); }; Chat.ipb_sendChat = Chat.sendChat; Chat.sendChat = Chat.ext_send; Chat.ext_rewired = true; console.log('okay');} catch ( ex ) { console.log("Error: " + ex); console.log((new Error).stack);}})();commands: kick, ban, quit, and users
escaping: e, n, t, e, and \
emots: lenny, wat, shrug, whee, flip, and why



As usual, add this as a greasemonkey script or dump it into your javascript console

fastcall22
Beep boop, making progress on my FM synthesizer. Had a nasty popping sound with my phase-feedback logic and needed some way to rapidly graph out the logic. Powershell to the rescue:

# plot-sample.ps1$samples = 1024;$width = 2048;$height = 512;[Reflection.Assembly]::LoadWithPartialName("System.Drawing");$bmp = new-object Drawing.Bitmap $width, $height;$g = [Drawing.Graphics]::FromImage($bmp);$g.SmoothingMode = "AntiAlias";$penAxis = new-object Drawing.Pen 0xFF808080, 2;$penLineA = new-object Drawing.Pen 0xFF0000F0, 2;$penLineB = new-object Drawing.Pen 0xFFF00000, 2;$ptsA = new-object Drawing.PointF[] $samples;$ptsB = new-object Drawing.PointF[] $samples;0..($samples-1) |% { $Q = 4*[Math]::pi/$samples; $feedback = 0;} { $ampA = [Math]::sin($_*$Q + $feedback); # A[t] = sin(t + 1.05*A[t-1]) $ampB = [Math]::sin($_*$Q); # B[t] = sin(t) $feedback = $ampA * 1.05; $x = $bmp.Width * $_/$samples; $yA = $bmp.Height * (-$ampA/2.1 + .5); $yB = $bmp.Height * (-$ampB/2.1 + .5); $ptsA[$_] = new-object Drawing.PointF $x, $yA; $ptsB[$_] = new-object Drawing.PointF $x, $yB;} { $g.Clear([Drawing.Color]::White); $g.DrawLine($penAxis,0,$bmp.Height/2,$bmp.Width,$bmp.Height/2); $g.DrawLines($penLineB,$ptsB); $g.DrawLines($penLineA,$ptsA); $bmp.Save("$pwd\plot.png");}plot.png
Looks like the logic should be sound. I did find that the phase-feedback wasn't being sin'd in the synthesizer, and that was causing the popping noise. And here's a sample of what it sounds like right now:
fm-sample.ogg (36.4KB)

And here's the instrument used to generate that sound:
/*unit: mul frequency multiplier ofs additive frequency amp volume feed phase feedbackenvelope: A attack rate, in degrees D1 decay rate to sustain level, in degrees D2 decay rate to silence, in degrees R release rate, in degrees S sustain levelprogram: u unit to render in phase buffer input slot, 0 for null input out phase buffer output slot, 0 for audio data*/instrument i;int idx = 0; // mul ofs amp feed A D1 D2 R Si.units[idx++] = { 2.00f, 0.00f, 0.50f, 1.00f, {80.0, 20.0, 10.0, 45.0, 0.75 }};i.units[idx++] = { 1.00f, 0.00f, 0.25f, 0.00f, {85.0, 45.0, 5.0, 60.0, 0.50 }};i.units[idx++] = { 7.00f, 0.33f, 0.12f, 0.00f, {88.5, 60.0, 5.0, 60.0, 0.25 }};i.units[idx++] = { 1.00f, 0.33f, 0.50f, 0.00f, {87.0, 30.0, 5.0, 60.0, 0.67 }};i.ct_units = idx;// program:// ????// ????? ????// ?? 0??? 1???// ???? ???? ? ????// ??? 3??? out// ???? ? ????// ? 2???// ????idx = 0; // u in outi.program[idx++] = { 0, 0, 2 };i.program[idx++] = { 1, 2, 1 };i.program[idx++] = { 2, 0, 1 };i.program[idx++] = { 3, 1, 0 };i.ct_program = idx;Hue.
fastcall22
This a continuation of the Yet Another Generic Space Shooter (YAGSS) game.

Most of the graphics programming I've learned has been done through software rendering and fixed-function OpenGL, so I wasn't sure what to expect when writing my first real vertex shader. The sprites themselves would be simple, only having x, y, rotation, and scale as properties, and a simple tinting effect.

It was confusing at first to get the input layout just right for both of the two vertex buffers, the vertex and instance buffers. It turns out that InstanceDataStepRate is very important, otherwise you'll draw the same instance repeatedly. Oops.

As for the vertex shader itself, I had the greatest idea ever: Why not have the GPU do the between-state interpolation? The properties are simple enough, and it would save bandwidth, as I'd only need to update the constant buffer containing the interpolation amount in between frames, rather than the entire instances' state every frame. To do this, I packed the sprite's position, rotation, and scale into a single float4. Doubling this, I have a previous and current state. From there, I can interpolate between the two states.

// bufferscbuffer constants { float4 camera_transform; // (x,y,z,w) = (x,y,scale_x,scale_y) float4 time; // (x,y,z,w) = (app_time,game_time,interp,0)};// typesstruct in_instance { // vertex float2 pos : POSITION; float4 color : COLOR; // instance // 0 previous state, 1 current state (backwards for some reason, w/e) float4 transform[2] : TEXCOORD0; // (x,y,z,w) = (x,y,rotation,scale) float4 tint[2]: TEXCOORD2;};struct in_pixel { float4 pos : SV_POSITION; float4 color : COLOR;};// mainin_pixel main( in in_instance IN ) { // interpolate float interp = time.z; float4 t = lerp(IN.transform[1],IN.transform[0],interp); float4 tint = lerp(IN.tint[1],IN.tint[0],interp); // setup model transform float c = cos(t.z) * t.w; float s = sin(t.z) * t.w; float2 U = float2( c, s ); float2 V = float2( -s, c ); // transform float2 vpos = IN.pos; float2 pos = U*vpos.x + V*vpos.y + t.xy - camera_transform.xy; pos /= camera_transform.zw; // output in_pixel OUT; OUT.pos = float4(pos,0,1); OUT.color = float4(lerp(IN.color.rgb,tint.rgb,tint.a),IN.color.a); return OUT;}The first sprite shader. Textures not included.

The update logic went something like this:

bool did_update = false;for ( ticks_bucket += ticks_elapsed; ticks_bucket >= ticks_per_update; ticks_bucket -= ticks_per_update ) { update(time_per_update); // mutates instance array did_update = true;}if ( did_update ) update_instance_buffer(); // copies instance array directly to vertex bufferfloat interp = ticks_bucket / (float)ticks_per_update;shader_constants.interp = interp;update_constant_buffers();render(interp);Game loop excerpt (pseudo).

What I didn't realize until rewriting the shader, was that there was a significant performance penalty because the model's transform is rebuilt for every instance's vertex, even though there were only four vertices. I decided to see how it compared to a more "standard" approach. Here's what the second approach looked like:

// bufferscbuffer constants { row_major float2x3 camera_transform; row_major float2x3 atlas_transform; // TODO: remove this, sprite atlas should be normalized floats float4 time; // (x,y,z,w) = (app_time,game_time,interp,0)};// typesstruct in_instance { // vertex float2 pos : POSITION; float2 texcoord : TEXCOORD0; float4 color : COLOR; // instance row_major float2x3 transform : TEXCOORD1; row_major float2x3 tex_transform : TEXCOORD3; float4 tint : TEXCOORD5;};struct in_pixel { float4 pos : SV_POSITION; float4 color : COLOR; float2 texcoord : TEXCOORD;};// mainin_pixel main( in in_instance IN ) { // transform position in_pixel OUT; float2 pos = IN.pos; pos = mul(IN.transform, float3(pos,1)); pos = mul(camera_transform, float3(pos,1)); // transform texture float2 texcoord = IN.texcoord; texcoord = mul(IN.tex_transform, float3(texcoord,1)); texcoord = mul(atlas_transform, float3(texcoord,1)); // OUT.pos = float4(IN.camera_transform * float3(IN.transform*float3(IN.pos,1),1),0,1); OUT.pos = float4(pos,0,1); OUT.color = float4(lerp(IN.color.rgb,IN.tint.rgb,IN.tint.a),IN.color.a); OUT.texcoord = texcoord; return OUT;}Second attempt. Includes textures and spooky matrix math!

And the game loop:

// updatefor ( ticks_bucket += ticks_elapsed; ticks_bucket >= ticks_per_update; ticks_bucket -= ticks_per_update ) update(time_per_frame);float interp = ticks_bucket / (float)ticks_per_update;update_constant_buffer();copy(begin(entities),end(entities),begin(instances),entity_renderer(interp));update_instance_buffer();render(interp);Compared to the first attempt (and before textures were added in to the shader), I was able to push out 38% more untextured sprites from ~55000 to ~76000 sprites. It was then I decided that I probably won't even reach 3% of that number for this game. Oh well. Now about this second approach...
The interpolation is done inside the entity_renderer, taking entities and translating their interpolatable properties into instances to be sent to the GPU. The matrix-vector math was tricky, but by arranging it like so, I was able to shave off a row from each matrix:

[[px] [py] [ 1]][[Ux Vx dx] [[Ux*px + Vx*py + dx] [Uy Vy dy]] [Uy*px + Vy*py + dy]]Anyways, here's the engine now. Textures 'n' stuff:
f70ncEu.png

I'm still playing with the blending modes. And, I may end up ditching baking the glow effect into the textures. I might be able to save on fillrate by using a fullscreen shader effect, rather than rendering a bunch of bloated sprites, just for their glow effects.

In any case, I guess I should start actually building the game now...
fastcall22
?

I'm writing yet another generic space shooter while learning WTL, DirectX, and modern graphics programming along the way. Today was spent writing a script to build SVG fragments into a texture atlas. Here's the result of today's work:



atlas_sample.png


Sample generated atlas generated from SVG fragments

SVG fragment for "box"


[subheading]The game[/subheading]
This game is a mix of shoot-em-up, adventure, with some RPG, resource management, and some puzzle elements. Here's the engine so far:




game.png


75625 unique untextured sprites, averaging 70 FPS

I had prototyped a similar game in immediate mode OpenGL many years ago. The most tedious thing I remember from that project was that I had to rerender every sprite by hand every time I tweaked the style. It wasn't unusual to have sprites with different stroke widths and inconsistent glows.

With this game, I wanted to avoid all that nonsense by generating assets automatically. This way, I could tweak and adjust the style and sizes, without having to visit each asset by hand and rerendering them.


[subheading]The Objectives[/subheading]
The objectives are simple:
1. Reusable SVG template. A skeleton SVG file which will be used to generate fully-formed SVG elements. This will include the style, and the filters. A script will apply SVG fragments and dimensions to the skeleton and feed this to the SVG rasterizer.
2. Automated SVG rasterization. A library which will transform an SVG into a PNG. A library that accepts a SVG as a string instead of a file is preferred, but not necessary.
3. Texture atlas. The resulting PNGs will be packed into a single texture, and will also output an accompanying layout file.


[subheading]The Script[/subheading]
Because these assets will be compiled and assembled on my development machine, I chose to write the script in nodejs, because of its vast selection of libraries.


[subheading]The libraries[/subheading]
From what I could find and scan, following libraries that do most of the heavy lifting are javascript wrappers that facilitate inter-process communication between nodejs and the actual binary. It's not really a concern, as almost all dependencies, except imagemagick, were installed from node's package manager. Here are the libraries I chose for this script:

1. svg-to-png. Uses chromimum to render SVG files to PNGs. Imagemagick can render SVGs as well, but I didn't learn this until the end of the day.
2. binpacking.
3. gm with imagemagick. Wrapper library to wrap image rendering operations. Used to generate the texture atlas.
4. underscore. Provides functional-like methods and the templating engine.


[subheading]The Style[/subheading]
Here's the first iteration of the style.



style.png


Player's ship sprite. Notice that the stroke and blur effect expands outside of the canvas and the geometry snaps nicely to the canvas edges.


[subheading]The Template[/subheading]
From here, I exported an optimized SVG from Inkscape and split out the template and guts into two separate files, while additionally factoring out the common path attributes to a stylesheet.
<%= frag %> Initial SVG template. The <%= %> tags are evaluated dynamically.



SVG fragment for the player's sprite. The comment at the beginning signifies to the script the area this sprite's geometry occupies.

It took quite a few tries to get the filter and feGaussianBlur to behave correctly, especially with esoteric attributes on the filter tags. In retrospect, it may have been easier to keep the fragments as full Inkscape SVG documents and had the script extract the geometry and filter out attributes. This would also have elimiated the need for the size comment.


[subheading]The Script[/subheading]
And finally, this ugly beast of a script:


(function(){ // dependencies var fs = require('fs'), rasterizer = require('svg-to-png'), binpacking = require('binpacking'), gm = require('gm').subClass({imageMagick:true}), u = require('underscore'); // config var E = function(){}, defs_dir = 'defs', temp_dir = 'temp', ends_with = function(str) { return function(val) { return val.substr(val.length-str.length,val.length) == str; } }; // prepare svg template var build = u.template(''+fs.readFileSync('template.svgt')), outline_px = 4, outline_u = 1, blur_offset_u = 1.5; // compile defs into templates console.log('compiling...'); var compiled = []; u.filter(fs.readdirSync(defs_dir),ends_with('.svgfrag')) .forEach(function(name){ // load frag, find viewbox var frag = ''+fs.readFileSync(defs_dir+'/'+name); var box = (/^/.exec(frag) || []).slice(1,5); // parse viewbox: box=left, bottom, right, top if ( box.length != 4 ) { console.error('size meta malformed in '+name); return; } for ( var kdx in box ) { var val = parseFloat(box[kdx]); if ( isNaN(val) ) { console.error('size parameter #'+kdx+' invalid'); return; } box[kdx] = val; } // calculate viewbox and texture size var T = blur_offset_u + outline_u/2; box[0] -= T; box[1] -= T; box[2] += T; box[3] += T; var vw = box[2] - box[0], vh = box[3] - box[1], tw = Math.floor(vw*outline_px), th = Math.floor(vh*outline_px) // stuff frag into template var svg = build(tmp = { vx: box[0], vy: box[1], vw: vw, vh: vh, tw: tw, th: th, frag: frag, }); // record output var base_name = name.replace(/\..+$/, ''); var out_name = base_name+'.svg'; var out_svg = temp_dir+'/'+out_name; var out_png = temp_dir+'/'+base_name+'.png'; var cwd = process.cwd(); compiled.push({ name: base_name, svg: process.cwd()+'/'+out_svg, // ??? png: out_png, w: tw+2, h: th+2, }); // save to file fs.writeFileSync(out_svg, svg); }); // render compiled svgs to pngs console.log('rendering...'); try { process.chdir('temp'); // ??? rasterizer .convert(u.pluck(compiled,'svg'),'../..') // ??? .then(function(){ // sort and pack compiled svgs console.log('packing...'); var packer = new binpacking.GrowingPacker(); compiled.sort(function(a,b){ if ( a.w < b.w ) return 1; if ( a.w > b.w ) return -1; if ( a.h < b.h ) return 1; if ( a.h > b.h ) return -1; return 0; }); packer.fit(compiled); // render packed images to an atlas // and build map for atlas var img = gm(packer.root.w+1,packer.root.h+1,"#00000000"); var atlas = ''; for ( var cdx in compiled ) { var c = compiled[cdx]; var f = c.fit; if ( !f.used ) { console.error('unused: ', c); continue; } img .stroke('#FFFF00FF',1) .fill('#FFFFFF00') .drawRectangle(f.x,f.y,f.x+c.w,f.y+c.h) .draw('image Copy '+(f.x+1)+','+(f.y+1)+' 0,0 '+c.png); atlas += [c.name,f.x,f.y,f.w,f.h].join("\t"); atlas += "\n"; } img.write('atlas.png',function(e){ if ( e ) console.error(e); console.log('done'); }); fs.writeFileSync('atlas.txt', atlas); }); } finally { process.chdir('..'); }})();Svg-to-png was especially dumb, because for some reason it expects at least two levels of directories for input files and output directory, or something dumb like that. Had to do some chdir to trick it to output to the correct directory. Specifying inputs ['temp/a.svg','temp/b.svg'] and output 'temp' from working directory 'resources/svg' would place 'a.png' in 'temp/resources/svg/a.png'. Yeah, that was fun. No documentation on this behavior, either.


[subheading]Post-Mortem[/subheading]
Overall, the day was well spent. I got a script that works while learning about nodejs, imagemagick, and et. al. I can tweak and change the style at any time and rebuild the assets. Extracting the SVG fragments from Inkscape was a bit of a pain: Sometimes the coordinates of the optimized SVG were translated 10 units +y or 1000 units +y for no reason. Other than that, I'm happy with the results.


Also, what's a content pipeline?
And how do I IPBoard journal??
fastcall22

So I finally sat down and spent a Saturday morning extending IPBoard chat client. It works by rerouting IPBoard.prototype.chat to a custom function, which then transforms the message text through a series of WTF-regexes. This script should be installed as a Greasemonkey script, but it could also be copy-pasta'd into a developer tool's javascript window.
 

Features

 


Extra commands:

 

 

/users
/kick regex
/ban regex
/confirm
/quit
/mute

 


Text emotes:

 

 

:lenny: ( ?? ?? ??)
:wat: ?__?

 


Backslash escaping:

 

 

\n Newline
\e Zero width space (for escaping)
\t Em-width space
\xYYYY Equivalent to &#YYYY;

 


Support for custom BBCode tags.


Here is the script:
// ==UserScript==// @name IPBoard Chat Extensions// @namespace fastcall22.com// @description For GAMSDEV// @include http://www.gamedev.net/chat/// @version 1// @grant none// ==/UserScript==(function ipb_ext(){ var Chat = IPBoard.prototype.chat; // util Chat.ext_post_message = function(textContent) { var post = document.createElement('li'); post.setAttribute('class','post chat-myown'); post.textContent = textContent; document.getElementById('storage_chatroom') .appendChild(post); post.scrollIntoView(false); }; Chat.ext_search_users = function(query) { var result = {}; var q_reg = new RegExp(query||'.'); for ( var id in Chat.forumIdMap._object ) { var name = Chat.nameFormatting._object[id][2]; if ( q_reg.test(name) ) result[name] = id; } return result; }; Chat.ext_user_action = function(action,target_id){ ipb.chat.sendMessageToChild( "server="+serverHost + "&path="+serverPath + "&room="+roomId + "&user="+userId + "&access_key=" + accessKey + "&action="+action + "&against="+target_id ); ipb.chat.lastAction = parseInt(new Date().getTime().toString().substring(0, 10)); }; // config var null_f = function(){}; Chat.ext_empty_event = { stopPropagation: null_f, preventDefault: null_f }; Chat.ext_confirm_action = null; var make_action = function(action){ return function(query) { if ( !query.trim() ) return; var users = Chat.ext_search_users(query); Chat.ext_confirm_action = function() { for ( var k in users ) Chat.ext_user_action(action,users[k]); }; var str = ''; for ( var k in users ) { if ( str ) str += ', '; str += k; } Chat.ext_post_message("Send /confirm to "+action+": "+str+". Send /cancel to ignore"); }; }; Chat.ext_commands = { kick: make_action('kick'), ban: make_action('ban'), test: function() {Chat.test = !(Chat.test || false)}, debug: function() {Chat.debug = !(Chat.debug || false)}, users: function(query) { var list = Chat.ext_search_users(query); var str = ''; for ( var k in list ) { if ( str ) str += ', '; str += k; } Chat.ext_post_message(str); }, confirm: function() { if ( !Chat.ext_confirm_action ) return; Chat.ext_confirm_action(); Chat.ext_confirm_action = null; }, cancel: function() { Chat.ext_confirm_action = null; }, mute: function() { document.getElementById('sound_toggle').click(); }, quit: function(){ document.getElementById('leave_room').click(); }, }; Chat.ext_escape = { 'n': '\n', 't': '?', 'e': '?', }; Chat.ext_emotes = { fear: ':f34r:', lenny: '( ?? ?? ??)', wat: '?__?', }; Chat.ext_bbc = { rick: function(param,content) { // never gonna give you up // never gonna let you down // never gonna run around // and desert you } }; // helpers Chat.bbc_filter = function ext_bbc_filter(_,tag,param,content) { var f = Chat.ext_bbc[tag]; if ( !f ) return _; return f(param,content); }; Chat.ext_filters = [ /* commands */ [ /^\/(\w+)\s*(.*)$/, function ext_command_filter(_,cmd,argstr) { var args = argstr.split(' '); var f = Chat.ext_commands[cmd]; if ( !f ) return _; f.apply(null,args); return ''; } ], /* emotes */ [ /:(\w+):/g, function ext_emote_filter(_,tag) { return Chat.ext_emotes[tag] || _; } ], /* backslash */ [ /\\(?:(x[0-9a-fA-f]{4})|([a-z]))/g, function ext_backslash_filter(_,hex,code) { if ( hex ) return '&#'+hex+';'; return Chat.ext_escape || _; } ], /* BBC w/ content */ [ /\[(\w+)(?:=(.+))?\](.*?)\[\/\1\]/g, Chat.bbc_filter ], /* self-closing BBC tags */ [ /\[(\w+)(?:=(.+))?\s*()\/\]/g, Chat.bbc_filter ], /* general */ [ /\t/g, '?' ] ]; Chat.ext_filter_message = function(str) { Chat.ext_filters.forEach(function(f){ if ( !str.length ) return; if ( Chat.debug ) console.log(f,str); str = String.prototype.replace.apply(str,f); }); return str; }; Chat.ext_send = function ext_send(e) { if ( e.preventDefault ) e.preventDefault(); if ( e.stopPropagation ) e.stopPropagation(); var message = (document.getElementById('message_textarea').value || '').trim(); if ( Chat.debug ) console.log('send',message); if ( !message ) return; var filtered = Chat.ext_filter_message(message) || null; document.getElementById('message_textarea').value = filtered; if ( filtered ) return Chat.ipb_sendChat.call(this,Chat.ext_empty_event); }; Chat.ipb_sendChat = Chat.sendChat; Chat.sendChat = Chat.ext_send;})();Notable points of interest:
Chat.ext_send
Chat.ext_filter_message
Chat.ext_filters


The extension is pretty dumb, but it works. So, have fun! smile.png