Initial commit illustrating LTG loading and masking
This project -- creating a LaserTank clone in JavaScript -- deserves some
discussion before diving into the implementation details (you may skip to the
separator below for implementation details without the history). LaserTank is a
game that has a special place in my heart. Not only is it a witty and enjoyable
game, but it is in fact the reason I began programming as a young boy in the
first place. While it is likely that I would have eventually picked up a
programming book for some other reason, I owe that point in time entirely to
this game.
Allow me to explain. At the age of 10, I would spend much of my time on the
Internet downloading various games and demos that may satisfy my interests (the
old days of CNET's download.com showed me many of those, I believe). One of
those games that immediately captivated me was LaserTank, but not purely for
reasons of gameplay. It had this wonderful feature that added so much potential
(and replayability) to the game -- a map editor.
I found myself enthralled with the map editor to the point where I spent all my
time creating maps rather than playing the game. What fascinated me was the
ability to essentially create portions of the game -- to tell the game what to
do and how to function. I was not able to create my own game using this editor,
but I felt like I was creating portions of it. That said, I soon realized that
it wasn't enough; I needed to do more.
It was the limitations of the map editor and the enjoyment of creating the maps
that caused me to convince my parents to take me to Barnes and Noble to purchase
my first programming book - Learn to Program with Visual Basic 6 by John Smiley
(N.B. Visual Basic is proprietary and I cannot recommend using it. I had no
knowledge of the evils of proprietary software back at that point in time.) My
parents had their doubts, but that only pushed me even harder to learn.
Ironically, the book was about creating a business application. I found this
process very enjoyable and began focusing more on conventional desktop software
rather than gaming. I did create a couple games (bop-a-mole and breakout among
others), but my focus was never game development. Eventually, I moved on from
Visual Basic and got into web development. Following that, I discovered
GNU/Linux and began getting more and more into lower-level systems and languages
(such as C and ASM) and began adopting the hacker ethic. That brings us to where
I am today -- nearly 13 years later. It would only seem fitting to bring my
hobby-turned-career full circle by cloning the very game that started
everything.
When I say "clone", I mean nothing more; there will be no additional features
or modifications to the original gameplay, menus, graphics, etc. It will support
all original file formats (I will develop none of my own). The only differences
between the clone and the original game will arise from the obvious issues
introduced by cloning the game on a web platform. Specifically, the user will
have the option to load files from either their local box or a remote resource,
and I may provide pre-masked tile sets for browsers that do not support the
canvas element (a fallback mode, if you will). No matter what the change,
though, the gameplay will remain identical.
That said, the library resulting from the clone will be built with extensibility
in mind. If a user (or myself) wishes to create a derivative work by hooking or
extending the library (for example, to support larger maps, add additional
blocks/enemies, multiplayer support, etc), that should be fairly trivial to do.
However, those works will be entirely separate from the clone and clearly
distinguished. I think the original LaserTank is perfect the way it is.
Remember, it has a special place in my heart (aww) and I would like to preserve
the game as I remember it back then.
---
Alright; now that we have a great deal of unnecessary history out of the way,
let's get into the implementation details for this commit (if you're reading
this as a blog entry, see the first commit). This commit represents a
proof-of-concept showing that the LTG files (containing the LaserTank graphics)
can be properly loaded and their masks properly applied. This was the first
major concern for the project and, if a workaround were needed, would have
prevented me from creating a full clone (as it would not support loading LTG
files without having them first sent to the server, processed, and returned in a
different format).
The LTG file contains some metadata (including the name, author and description)
as well as two bitmaps (BMPs) -- the game tileset and the associated mask. The
game bitmap's position was static, but its length and the offset of the mask
bitmap were provided by the four bytes immediately preceding the game bitmap
(the TLTGREC struct in LTANK.H of the original sources represented this value as
a DWORD, which represents a 32-bit integer). The only challenge with converting
this value into an integer that we could use is its endianness -- is the most
significant byte at the beginning or end? Windows programs (of which LaserTank
is) generally write in little-endian format, but to be sure we can simply open
up the LTG file in your favorite HEX editor (I simply use `xxd`). In the case of
the original tileset, the four bytes immediately preceding the bitmap header (as
identified by 'BM', or `424d`) were `7a f5 00 00`, which on its own clearly
indicates little-endianness. We can verify by searching for 'BM' once again, and
finding that it begins at location `f5 7a` (if your HEX editor displays in
big-endian format). To convert into a number, we can simply add up each byte
individually, left-shifting by 8N bits, where N is the 0-indexed byte position.
Loading the BMP files was then fairly trivial; the file could be loaded into
memory (read from disk using FileReader) and we could cut the relevant portions
of the binary string out. The bitmaps could then be base64-encoded and the
"src" attribute of an Image object set to 'data:image/bmp;base64,B', where B
is the base64-encoded BMP. This could then be rendered however we please - CSS
sprites or to a canvas.
The problem with CSS sprites is that we need to apply the mask and there is no
reliable way to do this without a canvas; transparency in the browser is
normally handled using GIFs or PNGs. As it turns out, the canvas performs
masking using the alpha channel as well, so I would have to create my own
masking algorithm to manipulate the alpha channel of the tileset. To complicate
matters even more, certain tiles had no mask, and they did not consistently
represent the mask with all black (black is used in LT to indicate opacity),
meaning that the algorithm would have to understand what tiles should be skipped
entirely.
The solution was to simply loop through each tile and set the alpha byte of each
pixel relative to the respective pixel on the map. Because the images were
created out of data in memory, the canvas is not tainted when the image is
rendered before using getImageData(). To help speed up the process, since we
know that the mask can only contain black and white, we need only check one of
the channels; we do not need to calculate brightness. This process successfully
returns each individual tile, properly masked, which can be rendered to the
canvas using putImageData(). Crisis averted. With that major concern out of the
way, the clone should no longer be a problem.
I go into more detail in the comments within LtgLoader and TileMasker.
This is going to be an exciting process, both because of the LT clone itself and
because this is my first experience working with the canvas element. As
aforementioned, I seldom create games and I have had no use for the canvas
thus far.
Hopefully this project will be well-received by both those who have played
LaserTank in the past and the original developer of the game (Jim Kindley, JEK
Software). LaserTank is a fairly old game and is not likely to be well known
anymore, but the game itself is a blast (no pun intended) and bringing it to the
browser, where it can be used on any platform (including mobile devices), should
allow everyone to enjoy it.
The source code is released under the GNU AGPL to ensure that the users'
freedoms are preserved even if this game is rendered or in any way run
server-side.
2012-03-11 21:38:06 -04:00
|
|
|
/**
|
|
|
|
* Handles the loading of LTG files
|
|
|
|
*
|
|
|
|
* Copyright (C) 2012 Mike Gerwitz
|
|
|
|
*
|
|
|
|
* This program is free software: you can redistribute it and/or modify
|
|
|
|
* it under the terms of the GNU Affero General Public License as published by
|
|
|
|
* the Free Software Foundation, either version 3 of the License, or
|
|
|
|
* (at your option) any later version.
|
|
|
|
*
|
|
|
|
* This program is distributed in the hope that it will be useful,
|
|
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
* GNU Affero General Public License for more details.
|
|
|
|
*
|
|
|
|
* You should have received a copy of the GNU Affero General Public License
|
|
|
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
*
|
|
|
|
*
|
|
|
|
* LTG files contain the game tiles and additional metadata. Specifically, the
|
|
|
|
* structure of the header is:
|
|
|
|
* - name (string), 40 bytes
|
|
|
|
* - author (string), 30 bytes
|
|
|
|
* - description (string), 245 bytes
|
|
|
|
* - id (string), 5 bytes, "LTG1\0"
|
|
|
|
* - mask offset (32-bit integer), 4 bytes (little-endian)
|
|
|
|
* ~ (see TLTGREC struct in LTANK.H of the original game sources)
|
|
|
|
*
|
|
|
|
* Immediately following the header is the game tile set (a bitmap), immediately
|
|
|
|
* after which we find the mask bitmap (at the mask offset).
|
|
|
|
*
|
|
|
|
* In the original game (written in C), the loading of this file into the
|
|
|
|
* necessary data structures was trivial and highly performant. In the case of
|
|
|
|
* ECMAScript, we are left to string parsing. With the string in memory, we will
|
|
|
|
* cut out the necessary segments.
|
|
|
|
*
|
|
|
|
* At that point, we can easily convert the binary bitmap data into usable
|
|
|
|
* images by creating new Image objects in memory and assigning the `src'
|
|
|
|
* attribute to "data:image/bmp;base64,B", where B is the base64-encoded bitmap.
|
|
|
|
* (To help visualize the data, one can open the LTG file in his/her favorite
|
|
|
|
* text editor and search for "BM" (0x424D), which is the header field used to
|
|
|
|
* identify the file as a bitmap image.)
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Loads tiles and metadata from LTG files
|
|
|
|
*/
|
|
|
|
ltjs.LtgLoader = Class( 'LtgLoader',
|
|
|
|
{
|
2012-03-18 00:35:01 -04:00
|
|
|
/** various data segment byte offsets and lengths **/
|
Initial commit illustrating LTG loading and masking
This project -- creating a LaserTank clone in JavaScript -- deserves some
discussion before diving into the implementation details (you may skip to the
separator below for implementation details without the history). LaserTank is a
game that has a special place in my heart. Not only is it a witty and enjoyable
game, but it is in fact the reason I began programming as a young boy in the
first place. While it is likely that I would have eventually picked up a
programming book for some other reason, I owe that point in time entirely to
this game.
Allow me to explain. At the age of 10, I would spend much of my time on the
Internet downloading various games and demos that may satisfy my interests (the
old days of CNET's download.com showed me many of those, I believe). One of
those games that immediately captivated me was LaserTank, but not purely for
reasons of gameplay. It had this wonderful feature that added so much potential
(and replayability) to the game -- a map editor.
I found myself enthralled with the map editor to the point where I spent all my
time creating maps rather than playing the game. What fascinated me was the
ability to essentially create portions of the game -- to tell the game what to
do and how to function. I was not able to create my own game using this editor,
but I felt like I was creating portions of it. That said, I soon realized that
it wasn't enough; I needed to do more.
It was the limitations of the map editor and the enjoyment of creating the maps
that caused me to convince my parents to take me to Barnes and Noble to purchase
my first programming book - Learn to Program with Visual Basic 6 by John Smiley
(N.B. Visual Basic is proprietary and I cannot recommend using it. I had no
knowledge of the evils of proprietary software back at that point in time.) My
parents had their doubts, but that only pushed me even harder to learn.
Ironically, the book was about creating a business application. I found this
process very enjoyable and began focusing more on conventional desktop software
rather than gaming. I did create a couple games (bop-a-mole and breakout among
others), but my focus was never game development. Eventually, I moved on from
Visual Basic and got into web development. Following that, I discovered
GNU/Linux and began getting more and more into lower-level systems and languages
(such as C and ASM) and began adopting the hacker ethic. That brings us to where
I am today -- nearly 13 years later. It would only seem fitting to bring my
hobby-turned-career full circle by cloning the very game that started
everything.
When I say "clone", I mean nothing more; there will be no additional features
or modifications to the original gameplay, menus, graphics, etc. It will support
all original file formats (I will develop none of my own). The only differences
between the clone and the original game will arise from the obvious issues
introduced by cloning the game on a web platform. Specifically, the user will
have the option to load files from either their local box or a remote resource,
and I may provide pre-masked tile sets for browsers that do not support the
canvas element (a fallback mode, if you will). No matter what the change,
though, the gameplay will remain identical.
That said, the library resulting from the clone will be built with extensibility
in mind. If a user (or myself) wishes to create a derivative work by hooking or
extending the library (for example, to support larger maps, add additional
blocks/enemies, multiplayer support, etc), that should be fairly trivial to do.
However, those works will be entirely separate from the clone and clearly
distinguished. I think the original LaserTank is perfect the way it is.
Remember, it has a special place in my heart (aww) and I would like to preserve
the game as I remember it back then.
---
Alright; now that we have a great deal of unnecessary history out of the way,
let's get into the implementation details for this commit (if you're reading
this as a blog entry, see the first commit). This commit represents a
proof-of-concept showing that the LTG files (containing the LaserTank graphics)
can be properly loaded and their masks properly applied. This was the first
major concern for the project and, if a workaround were needed, would have
prevented me from creating a full clone (as it would not support loading LTG
files without having them first sent to the server, processed, and returned in a
different format).
The LTG file contains some metadata (including the name, author and description)
as well as two bitmaps (BMPs) -- the game tileset and the associated mask. The
game bitmap's position was static, but its length and the offset of the mask
bitmap were provided by the four bytes immediately preceding the game bitmap
(the TLTGREC struct in LTANK.H of the original sources represented this value as
a DWORD, which represents a 32-bit integer). The only challenge with converting
this value into an integer that we could use is its endianness -- is the most
significant byte at the beginning or end? Windows programs (of which LaserTank
is) generally write in little-endian format, but to be sure we can simply open
up the LTG file in your favorite HEX editor (I simply use `xxd`). In the case of
the original tileset, the four bytes immediately preceding the bitmap header (as
identified by 'BM', or `424d`) were `7a f5 00 00`, which on its own clearly
indicates little-endianness. We can verify by searching for 'BM' once again, and
finding that it begins at location `f5 7a` (if your HEX editor displays in
big-endian format). To convert into a number, we can simply add up each byte
individually, left-shifting by 8N bits, where N is the 0-indexed byte position.
Loading the BMP files was then fairly trivial; the file could be loaded into
memory (read from disk using FileReader) and we could cut the relevant portions
of the binary string out. The bitmaps could then be base64-encoded and the
"src" attribute of an Image object set to 'data:image/bmp;base64,B', where B
is the base64-encoded BMP. This could then be rendered however we please - CSS
sprites or to a canvas.
The problem with CSS sprites is that we need to apply the mask and there is no
reliable way to do this without a canvas; transparency in the browser is
normally handled using GIFs or PNGs. As it turns out, the canvas performs
masking using the alpha channel as well, so I would have to create my own
masking algorithm to manipulate the alpha channel of the tileset. To complicate
matters even more, certain tiles had no mask, and they did not consistently
represent the mask with all black (black is used in LT to indicate opacity),
meaning that the algorithm would have to understand what tiles should be skipped
entirely.
The solution was to simply loop through each tile and set the alpha byte of each
pixel relative to the respective pixel on the map. Because the images were
created out of data in memory, the canvas is not tainted when the image is
rendered before using getImageData(). To help speed up the process, since we
know that the mask can only contain black and white, we need only check one of
the channels; we do not need to calculate brightness. This process successfully
returns each individual tile, properly masked, which can be rendered to the
canvas using putImageData(). Crisis averted. With that major concern out of the
way, the clone should no longer be a problem.
I go into more detail in the comments within LtgLoader and TileMasker.
This is going to be an exciting process, both because of the LT clone itself and
because this is my first experience working with the canvas element. As
aforementioned, I seldom create games and I have had no use for the canvas
thus far.
Hopefully this project will be well-received by both those who have played
LaserTank in the past and the original developer of the game (Jim Kindley, JEK
Software). LaserTank is a fairly old game and is not likely to be well known
anymore, but the game itself is a blast (no pun intended) and bringing it to the
browser, where it can be used on any platform (including mobile devices), should
allow everyone to enjoy it.
The source code is released under the GNU AGPL to ensure that the users'
freedoms are preserved even if this game is rendered or in any way run
server-side.
2012-03-11 21:38:06 -04:00
|
|
|
'private const _POS_NAME': [ 0, 40 ],
|
|
|
|
'private const _POS_AUTHOR': [ 40, 30 ],
|
2012-03-18 00:35:01 -04:00
|
|
|
'private const _POS_DESC': [ 70, 245 ],
|
|
|
|
'private const _POS_ID': [ 315, 5 ],
|
|
|
|
'private const _POS_MOFF': [ 320, 4 ],
|
Initial commit illustrating LTG loading and masking
This project -- creating a LaserTank clone in JavaScript -- deserves some
discussion before diving into the implementation details (you may skip to the
separator below for implementation details without the history). LaserTank is a
game that has a special place in my heart. Not only is it a witty and enjoyable
game, but it is in fact the reason I began programming as a young boy in the
first place. While it is likely that I would have eventually picked up a
programming book for some other reason, I owe that point in time entirely to
this game.
Allow me to explain. At the age of 10, I would spend much of my time on the
Internet downloading various games and demos that may satisfy my interests (the
old days of CNET's download.com showed me many of those, I believe). One of
those games that immediately captivated me was LaserTank, but not purely for
reasons of gameplay. It had this wonderful feature that added so much potential
(and replayability) to the game -- a map editor.
I found myself enthralled with the map editor to the point where I spent all my
time creating maps rather than playing the game. What fascinated me was the
ability to essentially create portions of the game -- to tell the game what to
do and how to function. I was not able to create my own game using this editor,
but I felt like I was creating portions of it. That said, I soon realized that
it wasn't enough; I needed to do more.
It was the limitations of the map editor and the enjoyment of creating the maps
that caused me to convince my parents to take me to Barnes and Noble to purchase
my first programming book - Learn to Program with Visual Basic 6 by John Smiley
(N.B. Visual Basic is proprietary and I cannot recommend using it. I had no
knowledge of the evils of proprietary software back at that point in time.) My
parents had their doubts, but that only pushed me even harder to learn.
Ironically, the book was about creating a business application. I found this
process very enjoyable and began focusing more on conventional desktop software
rather than gaming. I did create a couple games (bop-a-mole and breakout among
others), but my focus was never game development. Eventually, I moved on from
Visual Basic and got into web development. Following that, I discovered
GNU/Linux and began getting more and more into lower-level systems and languages
(such as C and ASM) and began adopting the hacker ethic. That brings us to where
I am today -- nearly 13 years later. It would only seem fitting to bring my
hobby-turned-career full circle by cloning the very game that started
everything.
When I say "clone", I mean nothing more; there will be no additional features
or modifications to the original gameplay, menus, graphics, etc. It will support
all original file formats (I will develop none of my own). The only differences
between the clone and the original game will arise from the obvious issues
introduced by cloning the game on a web platform. Specifically, the user will
have the option to load files from either their local box or a remote resource,
and I may provide pre-masked tile sets for browsers that do not support the
canvas element (a fallback mode, if you will). No matter what the change,
though, the gameplay will remain identical.
That said, the library resulting from the clone will be built with extensibility
in mind. If a user (or myself) wishes to create a derivative work by hooking or
extending the library (for example, to support larger maps, add additional
blocks/enemies, multiplayer support, etc), that should be fairly trivial to do.
However, those works will be entirely separate from the clone and clearly
distinguished. I think the original LaserTank is perfect the way it is.
Remember, it has a special place in my heart (aww) and I would like to preserve
the game as I remember it back then.
---
Alright; now that we have a great deal of unnecessary history out of the way,
let's get into the implementation details for this commit (if you're reading
this as a blog entry, see the first commit). This commit represents a
proof-of-concept showing that the LTG files (containing the LaserTank graphics)
can be properly loaded and their masks properly applied. This was the first
major concern for the project and, if a workaround were needed, would have
prevented me from creating a full clone (as it would not support loading LTG
files without having them first sent to the server, processed, and returned in a
different format).
The LTG file contains some metadata (including the name, author and description)
as well as two bitmaps (BMPs) -- the game tileset and the associated mask. The
game bitmap's position was static, but its length and the offset of the mask
bitmap were provided by the four bytes immediately preceding the game bitmap
(the TLTGREC struct in LTANK.H of the original sources represented this value as
a DWORD, which represents a 32-bit integer). The only challenge with converting
this value into an integer that we could use is its endianness -- is the most
significant byte at the beginning or end? Windows programs (of which LaserTank
is) generally write in little-endian format, but to be sure we can simply open
up the LTG file in your favorite HEX editor (I simply use `xxd`). In the case of
the original tileset, the four bytes immediately preceding the bitmap header (as
identified by 'BM', or `424d`) were `7a f5 00 00`, which on its own clearly
indicates little-endianness. We can verify by searching for 'BM' once again, and
finding that it begins at location `f5 7a` (if your HEX editor displays in
big-endian format). To convert into a number, we can simply add up each byte
individually, left-shifting by 8N bits, where N is the 0-indexed byte position.
Loading the BMP files was then fairly trivial; the file could be loaded into
memory (read from disk using FileReader) and we could cut the relevant portions
of the binary string out. The bitmaps could then be base64-encoded and the
"src" attribute of an Image object set to 'data:image/bmp;base64,B', where B
is the base64-encoded BMP. This could then be rendered however we please - CSS
sprites or to a canvas.
The problem with CSS sprites is that we need to apply the mask and there is no
reliable way to do this without a canvas; transparency in the browser is
normally handled using GIFs or PNGs. As it turns out, the canvas performs
masking using the alpha channel as well, so I would have to create my own
masking algorithm to manipulate the alpha channel of the tileset. To complicate
matters even more, certain tiles had no mask, and they did not consistently
represent the mask with all black (black is used in LT to indicate opacity),
meaning that the algorithm would have to understand what tiles should be skipped
entirely.
The solution was to simply loop through each tile and set the alpha byte of each
pixel relative to the respective pixel on the map. Because the images were
created out of data in memory, the canvas is not tainted when the image is
rendered before using getImageData(). To help speed up the process, since we
know that the mask can only contain black and white, we need only check one of
the channels; we do not need to calculate brightness. This process successfully
returns each individual tile, properly masked, which can be rendered to the
canvas using putImageData(). Crisis averted. With that major concern out of the
way, the clone should no longer be a problem.
I go into more detail in the comments within LtgLoader and TileMasker.
This is going to be an exciting process, both because of the LT clone itself and
because this is my first experience working with the canvas element. As
aforementioned, I seldom create games and I have had no use for the canvas
thus far.
Hopefully this project will be well-received by both those who have played
LaserTank in the past and the original developer of the game (Jim Kindley, JEK
Software). LaserTank is a fairly old game and is not likely to be well known
anymore, but the game itself is a blast (no pun intended) and bringing it to the
browser, where it can be used on any platform (including mobile devices), should
allow everyone to enjoy it.
The source code is released under the GNU AGPL to ensure that the users'
freedoms are preserved even if this game is rendered or in any way run
server-side.
2012-03-11 21:38:06 -04:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Beginning of game bitmap (one byte past the header)
|
|
|
|
* @type {number}
|
|
|
|
*/
|
|
|
|
'private const _OFFSET_HEADER_END': 324,
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Load LTG file from memory and return the raw data
|
|
|
|
*
|
|
|
|
* @param {string} ltg_data binary LTG data
|
|
|
|
*
|
|
|
|
* @return {Object} LTG metadata and bitmaps (sans mask offset)
|
|
|
|
*/
|
|
|
|
'public fromString': function( ltg_data )
|
|
|
|
{
|
|
|
|
var mask_offset = this._getMaskOffsetFromData( ltg_data );
|
|
|
|
|
|
|
|
return {
|
|
|
|
name: this._getNameFromData( ltg_data ),
|
|
|
|
author: this._getAuthorFromData( ltg_data ),
|
|
|
|
desc: this._getDescFromData( ltg_data ),
|
|
|
|
id: this._getIdFromData( ltg_data ),
|
|
|
|
|
|
|
|
tiles: this._getBitmapDataUrl(
|
|
|
|
this._getGameBitmap( ltg_data, mask_offset )
|
|
|
|
),
|
|
|
|
|
|
|
|
mask: this._getBitmapDataUrl(
|
|
|
|
this._getMaskBitmap( ltg_data, mask_offset )
|
|
|
|
)
|
|
|
|
};
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Retrieve the requested portion of the given data, optionally stripping
|
|
|
|
* null bytes
|
|
|
|
*
|
|
|
|
* @param {string} ltg_data source LTG data
|
|
|
|
* @param {string} sgmt name of segment to retrieve (constant)
|
|
|
|
* @param {=boolean} stripnull whether to strip null bytes (default true)
|
|
|
|
*
|
|
|
|
* @return {string} requested segment
|
|
|
|
*/
|
|
|
|
'private _getDataSegment': function( ltg_data, sgmt, stripnull )
|
|
|
|
{
|
|
|
|
// strip null bytes by default
|
2012-03-18 00:35:01 -04:00
|
|
|
stripnull = ( stripnull === undefined ) ? true : !!stripnull;
|
Initial commit illustrating LTG loading and masking
This project -- creating a LaserTank clone in JavaScript -- deserves some
discussion before diving into the implementation details (you may skip to the
separator below for implementation details without the history). LaserTank is a
game that has a special place in my heart. Not only is it a witty and enjoyable
game, but it is in fact the reason I began programming as a young boy in the
first place. While it is likely that I would have eventually picked up a
programming book for some other reason, I owe that point in time entirely to
this game.
Allow me to explain. At the age of 10, I would spend much of my time on the
Internet downloading various games and demos that may satisfy my interests (the
old days of CNET's download.com showed me many of those, I believe). One of
those games that immediately captivated me was LaserTank, but not purely for
reasons of gameplay. It had this wonderful feature that added so much potential
(and replayability) to the game -- a map editor.
I found myself enthralled with the map editor to the point where I spent all my
time creating maps rather than playing the game. What fascinated me was the
ability to essentially create portions of the game -- to tell the game what to
do and how to function. I was not able to create my own game using this editor,
but I felt like I was creating portions of it. That said, I soon realized that
it wasn't enough; I needed to do more.
It was the limitations of the map editor and the enjoyment of creating the maps
that caused me to convince my parents to take me to Barnes and Noble to purchase
my first programming book - Learn to Program with Visual Basic 6 by John Smiley
(N.B. Visual Basic is proprietary and I cannot recommend using it. I had no
knowledge of the evils of proprietary software back at that point in time.) My
parents had their doubts, but that only pushed me even harder to learn.
Ironically, the book was about creating a business application. I found this
process very enjoyable and began focusing more on conventional desktop software
rather than gaming. I did create a couple games (bop-a-mole and breakout among
others), but my focus was never game development. Eventually, I moved on from
Visual Basic and got into web development. Following that, I discovered
GNU/Linux and began getting more and more into lower-level systems and languages
(such as C and ASM) and began adopting the hacker ethic. That brings us to where
I am today -- nearly 13 years later. It would only seem fitting to bring my
hobby-turned-career full circle by cloning the very game that started
everything.
When I say "clone", I mean nothing more; there will be no additional features
or modifications to the original gameplay, menus, graphics, etc. It will support
all original file formats (I will develop none of my own). The only differences
between the clone and the original game will arise from the obvious issues
introduced by cloning the game on a web platform. Specifically, the user will
have the option to load files from either their local box or a remote resource,
and I may provide pre-masked tile sets for browsers that do not support the
canvas element (a fallback mode, if you will). No matter what the change,
though, the gameplay will remain identical.
That said, the library resulting from the clone will be built with extensibility
in mind. If a user (or myself) wishes to create a derivative work by hooking or
extending the library (for example, to support larger maps, add additional
blocks/enemies, multiplayer support, etc), that should be fairly trivial to do.
However, those works will be entirely separate from the clone and clearly
distinguished. I think the original LaserTank is perfect the way it is.
Remember, it has a special place in my heart (aww) and I would like to preserve
the game as I remember it back then.
---
Alright; now that we have a great deal of unnecessary history out of the way,
let's get into the implementation details for this commit (if you're reading
this as a blog entry, see the first commit). This commit represents a
proof-of-concept showing that the LTG files (containing the LaserTank graphics)
can be properly loaded and their masks properly applied. This was the first
major concern for the project and, if a workaround were needed, would have
prevented me from creating a full clone (as it would not support loading LTG
files without having them first sent to the server, processed, and returned in a
different format).
The LTG file contains some metadata (including the name, author and description)
as well as two bitmaps (BMPs) -- the game tileset and the associated mask. The
game bitmap's position was static, but its length and the offset of the mask
bitmap were provided by the four bytes immediately preceding the game bitmap
(the TLTGREC struct in LTANK.H of the original sources represented this value as
a DWORD, which represents a 32-bit integer). The only challenge with converting
this value into an integer that we could use is its endianness -- is the most
significant byte at the beginning or end? Windows programs (of which LaserTank
is) generally write in little-endian format, but to be sure we can simply open
up the LTG file in your favorite HEX editor (I simply use `xxd`). In the case of
the original tileset, the four bytes immediately preceding the bitmap header (as
identified by 'BM', or `424d`) were `7a f5 00 00`, which on its own clearly
indicates little-endianness. We can verify by searching for 'BM' once again, and
finding that it begins at location `f5 7a` (if your HEX editor displays in
big-endian format). To convert into a number, we can simply add up each byte
individually, left-shifting by 8N bits, where N is the 0-indexed byte position.
Loading the BMP files was then fairly trivial; the file could be loaded into
memory (read from disk using FileReader) and we could cut the relevant portions
of the binary string out. The bitmaps could then be base64-encoded and the
"src" attribute of an Image object set to 'data:image/bmp;base64,B', where B
is the base64-encoded BMP. This could then be rendered however we please - CSS
sprites or to a canvas.
The problem with CSS sprites is that we need to apply the mask and there is no
reliable way to do this without a canvas; transparency in the browser is
normally handled using GIFs or PNGs. As it turns out, the canvas performs
masking using the alpha channel as well, so I would have to create my own
masking algorithm to manipulate the alpha channel of the tileset. To complicate
matters even more, certain tiles had no mask, and they did not consistently
represent the mask with all black (black is used in LT to indicate opacity),
meaning that the algorithm would have to understand what tiles should be skipped
entirely.
The solution was to simply loop through each tile and set the alpha byte of each
pixel relative to the respective pixel on the map. Because the images were
created out of data in memory, the canvas is not tainted when the image is
rendered before using getImageData(). To help speed up the process, since we
know that the mask can only contain black and white, we need only check one of
the channels; we do not need to calculate brightness. This process successfully
returns each individual tile, properly masked, which can be rendered to the
canvas using putImageData(). Crisis averted. With that major concern out of the
way, the clone should no longer be a problem.
I go into more detail in the comments within LtgLoader and TileMasker.
This is going to be an exciting process, both because of the LT clone itself and
because this is my first experience working with the canvas element. As
aforementioned, I seldom create games and I have had no use for the canvas
thus far.
Hopefully this project will be well-received by both those who have played
LaserTank in the past and the original developer of the game (Jim Kindley, JEK
Software). LaserTank is a fairly old game and is not likely to be well known
anymore, but the game itself is a blast (no pun intended) and bringing it to the
browser, where it can be used on any platform (including mobile devices), should
allow everyone to enjoy it.
The source code is released under the GNU AGPL to ensure that the users'
freedoms are preserved even if this game is rendered or in any way run
server-side.
2012-03-11 21:38:06 -04:00
|
|
|
|
|
|
|
if ( typeof sgmt === 'string' )
|
|
|
|
{
|
|
|
|
sgmt = this.__self.$( sgmt );
|
|
|
|
}
|
|
|
|
|
2012-03-18 00:35:01 -04:00
|
|
|
var data = String.prototype.substr.apply( ltg_data, sgmt );
|
|
|
|
|
|
|
|
return ( stripnull )
|
|
|
|
? data.split( '\x00' )[ 0 ]
|
|
|
|
: data;
|
Initial commit illustrating LTG loading and masking
This project -- creating a LaserTank clone in JavaScript -- deserves some
discussion before diving into the implementation details (you may skip to the
separator below for implementation details without the history). LaserTank is a
game that has a special place in my heart. Not only is it a witty and enjoyable
game, but it is in fact the reason I began programming as a young boy in the
first place. While it is likely that I would have eventually picked up a
programming book for some other reason, I owe that point in time entirely to
this game.
Allow me to explain. At the age of 10, I would spend much of my time on the
Internet downloading various games and demos that may satisfy my interests (the
old days of CNET's download.com showed me many of those, I believe). One of
those games that immediately captivated me was LaserTank, but not purely for
reasons of gameplay. It had this wonderful feature that added so much potential
(and replayability) to the game -- a map editor.
I found myself enthralled with the map editor to the point where I spent all my
time creating maps rather than playing the game. What fascinated me was the
ability to essentially create portions of the game -- to tell the game what to
do and how to function. I was not able to create my own game using this editor,
but I felt like I was creating portions of it. That said, I soon realized that
it wasn't enough; I needed to do more.
It was the limitations of the map editor and the enjoyment of creating the maps
that caused me to convince my parents to take me to Barnes and Noble to purchase
my first programming book - Learn to Program with Visual Basic 6 by John Smiley
(N.B. Visual Basic is proprietary and I cannot recommend using it. I had no
knowledge of the evils of proprietary software back at that point in time.) My
parents had their doubts, but that only pushed me even harder to learn.
Ironically, the book was about creating a business application. I found this
process very enjoyable and began focusing more on conventional desktop software
rather than gaming. I did create a couple games (bop-a-mole and breakout among
others), but my focus was never game development. Eventually, I moved on from
Visual Basic and got into web development. Following that, I discovered
GNU/Linux and began getting more and more into lower-level systems and languages
(such as C and ASM) and began adopting the hacker ethic. That brings us to where
I am today -- nearly 13 years later. It would only seem fitting to bring my
hobby-turned-career full circle by cloning the very game that started
everything.
When I say "clone", I mean nothing more; there will be no additional features
or modifications to the original gameplay, menus, graphics, etc. It will support
all original file formats (I will develop none of my own). The only differences
between the clone and the original game will arise from the obvious issues
introduced by cloning the game on a web platform. Specifically, the user will
have the option to load files from either their local box or a remote resource,
and I may provide pre-masked tile sets for browsers that do not support the
canvas element (a fallback mode, if you will). No matter what the change,
though, the gameplay will remain identical.
That said, the library resulting from the clone will be built with extensibility
in mind. If a user (or myself) wishes to create a derivative work by hooking or
extending the library (for example, to support larger maps, add additional
blocks/enemies, multiplayer support, etc), that should be fairly trivial to do.
However, those works will be entirely separate from the clone and clearly
distinguished. I think the original LaserTank is perfect the way it is.
Remember, it has a special place in my heart (aww) and I would like to preserve
the game as I remember it back then.
---
Alright; now that we have a great deal of unnecessary history out of the way,
let's get into the implementation details for this commit (if you're reading
this as a blog entry, see the first commit). This commit represents a
proof-of-concept showing that the LTG files (containing the LaserTank graphics)
can be properly loaded and their masks properly applied. This was the first
major concern for the project and, if a workaround were needed, would have
prevented me from creating a full clone (as it would not support loading LTG
files without having them first sent to the server, processed, and returned in a
different format).
The LTG file contains some metadata (including the name, author and description)
as well as two bitmaps (BMPs) -- the game tileset and the associated mask. The
game bitmap's position was static, but its length and the offset of the mask
bitmap were provided by the four bytes immediately preceding the game bitmap
(the TLTGREC struct in LTANK.H of the original sources represented this value as
a DWORD, which represents a 32-bit integer). The only challenge with converting
this value into an integer that we could use is its endianness -- is the most
significant byte at the beginning or end? Windows programs (of which LaserTank
is) generally write in little-endian format, but to be sure we can simply open
up the LTG file in your favorite HEX editor (I simply use `xxd`). In the case of
the original tileset, the four bytes immediately preceding the bitmap header (as
identified by 'BM', or `424d`) were `7a f5 00 00`, which on its own clearly
indicates little-endianness. We can verify by searching for 'BM' once again, and
finding that it begins at location `f5 7a` (if your HEX editor displays in
big-endian format). To convert into a number, we can simply add up each byte
individually, left-shifting by 8N bits, where N is the 0-indexed byte position.
Loading the BMP files was then fairly trivial; the file could be loaded into
memory (read from disk using FileReader) and we could cut the relevant portions
of the binary string out. The bitmaps could then be base64-encoded and the
"src" attribute of an Image object set to 'data:image/bmp;base64,B', where B
is the base64-encoded BMP. This could then be rendered however we please - CSS
sprites or to a canvas.
The problem with CSS sprites is that we need to apply the mask and there is no
reliable way to do this without a canvas; transparency in the browser is
normally handled using GIFs or PNGs. As it turns out, the canvas performs
masking using the alpha channel as well, so I would have to create my own
masking algorithm to manipulate the alpha channel of the tileset. To complicate
matters even more, certain tiles had no mask, and they did not consistently
represent the mask with all black (black is used in LT to indicate opacity),
meaning that the algorithm would have to understand what tiles should be skipped
entirely.
The solution was to simply loop through each tile and set the alpha byte of each
pixel relative to the respective pixel on the map. Because the images were
created out of data in memory, the canvas is not tainted when the image is
rendered before using getImageData(). To help speed up the process, since we
know that the mask can only contain black and white, we need only check one of
the channels; we do not need to calculate brightness. This process successfully
returns each individual tile, properly masked, which can be rendered to the
canvas using putImageData(). Crisis averted. With that major concern out of the
way, the clone should no longer be a problem.
I go into more detail in the comments within LtgLoader and TileMasker.
This is going to be an exciting process, both because of the LT clone itself and
because this is my first experience working with the canvas element. As
aforementioned, I seldom create games and I have had no use for the canvas
thus far.
Hopefully this project will be well-received by both those who have played
LaserTank in the past and the original developer of the game (Jim Kindley, JEK
Software). LaserTank is a fairly old game and is not likely to be well known
anymore, but the game itself is a blast (no pun intended) and bringing it to the
browser, where it can be used on any platform (including mobile devices), should
allow everyone to enjoy it.
The source code is released under the GNU AGPL to ensure that the users'
freedoms are preserved even if this game is rendered or in any way run
server-side.
2012-03-11 21:38:06 -04:00
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Retrieve LTG name from given LTG data
|
|
|
|
*
|
|
|
|
* @param {string} ltg_data raw LTG data
|
|
|
|
*
|
|
|
|
* @return {string} LTG name, null bytes stripped
|
|
|
|
*/
|
|
|
|
'private _getNameFromData': function( ltg_data )
|
|
|
|
{
|
|
|
|
return this._getDataSegment( ltg_data, '_POS_NAME' );
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Retrieve author name from the given LTG data
|
|
|
|
*
|
|
|
|
* @param {string} ltg_data raw LTG data
|
|
|
|
*
|
|
|
|
* @return {string} LTG author, null bytes stripped
|
|
|
|
*/
|
|
|
|
'private _getAuthorFromData': function( ltg_data )
|
|
|
|
{
|
|
|
|
return this._getDataSegment( ltg_data, '_POS_AUTHOR' );
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Retrieve description from the given LTG data
|
|
|
|
*
|
|
|
|
* @param {string} ltg_data raw LTG data
|
|
|
|
*
|
|
|
|
* @return {string} LTG description, null bytes stripped
|
|
|
|
*/
|
|
|
|
'private _getDescFromData': function( ltg_data )
|
|
|
|
{
|
|
|
|
return this._getDataSegment( ltg_data, '_POS_DESC' );
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Retrieve id from the given LTG data
|
|
|
|
*
|
|
|
|
* @param {string} ltg_data raw LTG data
|
|
|
|
*
|
|
|
|
* @return {string} LTG id, null bytes stripped
|
|
|
|
*/
|
|
|
|
'private _getIdFromData': function( ltg_data )
|
|
|
|
{
|
|
|
|
return this._getDataSegment( ltg_data, '_POS_ID' );
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Retrieve mask bitmap offset (relative to beginning of file) from the
|
|
|
|
* given LTG data
|
|
|
|
*
|
|
|
|
* The mask is stored as a 32-bit integer, little-endian.
|
|
|
|
*
|
|
|
|
* @param {string} ltg_data raw LTG data
|
|
|
|
*
|
|
|
|
* @return {number} LTG mask offset in bytes
|
|
|
|
*/
|
|
|
|
'private _getMaskOffsetFromData': function( ltg_data )
|
|
|
|
{
|
|
|
|
// grab the data and don't bother stripping off the null bytes (it would
|
|
|
|
// function the same with them stripped, but let's avoid the confusion
|
|
|
|
// since we are supposed to be working with a 32-bit value)
|
|
|
|
var data = this._getDataSegment( ltg_data, '_POS_MOFF', false ),
|
|
|
|
i = data.length,
|
|
|
|
offset = 0;
|
|
|
|
|
|
|
|
// convert the DWORD entry (little-endian format, 32-bit) into an
|
|
|
|
// integer that we can work with
|
|
|
|
while ( i-- )
|
|
|
|
{
|
|
|
|
offset += ( data.charCodeAt( i ) << ( 8 * i ) );
|
|
|
|
}
|
|
|
|
|
|
|
|
return offset;
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Return data URL for the given bitmap data
|
|
|
|
*
|
|
|
|
* The data URL may be used with any image element in place of an external
|
|
|
|
* resource. It consists of a "data:" prefix, MIME type and the
|
|
|
|
* base64-encoded data.
|
|
|
|
*
|
|
|
|
* @param {string} data binary bitmap data
|
|
|
|
*
|
|
|
|
* @return {string} data URL corresponding to the given bitmap data
|
|
|
|
*/
|
|
|
|
'private _getBitmapDataUrl': function( data )
|
|
|
|
{
|
|
|
|
return 'data:image/bmp;base64,' + btoa( data );
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Extracts the game bitmap from the given LTG data
|
|
|
|
*
|
|
|
|
* While the beginning offset of the game bitmap is static, the end is
|
|
|
|
* determined by the mask offset. The game bitmap would be displayed
|
|
|
|
* properly even if we read to the end of the file, but that is incorrect
|
|
|
|
* and poor practice.
|
|
|
|
*
|
|
|
|
* @param {string} ltg_data raw LTG data
|
|
|
|
* @param {number} mask_offset mask bitmap offset in bytes
|
|
|
|
*
|
|
|
|
* @return {string} game bitmap data
|
|
|
|
*/
|
|
|
|
'private _getGameBitmap': function( ltg_data, mask_offset )
|
|
|
|
{
|
|
|
|
var bmp_offset = this.__self.$( '_OFFSET_HEADER_END' );
|
|
|
|
|
|
|
|
// return the bitmap data up until the mask offset
|
|
|
|
return ltg_data.substr( bmp_offset, ( mask_offset - bmp_offset ) );
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Extracts the mask bitmap from the given LTG data
|
|
|
|
*
|
|
|
|
* The mask bitmap position must be provided and consists of the remainder
|
|
|
|
* of the file.
|
|
|
|
*
|
|
|
|
* @param {string} ltg_data raw LTG data
|
|
|
|
* @param {number} mask_offset mask bitmap offset in bytes
|
|
|
|
*
|
|
|
|
* @return {string} mask bitmap data
|
|
|
|
*/
|
|
|
|
'private _getMaskBitmap': function( ltg_data, mask_offset )
|
|
|
|
{
|
|
|
|
// the mask bitmap accounts for the remainder of the data
|
|
|
|
return ltg_data.substr( mask_offset );
|
|
|
|
}
|
|
|
|
} );
|