How Do You Work With Sprites, And Why Were They Used So Widely In Older Games?
Solution 1:
To answer your sprite sheet (also known as texture atlases) question briefly:
Do sprites by definition have the capabilities to do all the things like changing the color palette of a sprite, turning it backward or upside-down, etc?
Nope, you would have to still manually program that that. (The NES had helper instructions, p5.js doesn't currently have flip/rotate90 degrees functions as part of p5.Image AFAIK, however you could "cheat" and use a PGraphics buffer to draw into applying transformations (
translate()/rotate()/scale()
to achieve flipping and rotation)Is there any advantage to using sprites as opposed to using image display functions and just using png?
You would allocate memory for a sprite sheet once then simply reference to areas of that that later as frames need to be copied over (as opposed to many many independent images in a arrays, having load/decode the asset many times over). With more frames per character/game object and more game objects being able to efficiently pack pixels really saves on RAM allow you to use it for more fun gameplay mechanics and effects instead of just raw assets.
Why is it that they were so widely used in older hardware, and are they still used today in modern retro-styled games(Showelknight, Dead Cells, etc)?
Back then it was the constraint of the hardware so it was essential to economise on assets as much as possible to be able capture the audience with tight controls/gameplay mechanics and story. They're still used today for 3D video games and realtime graphics: the GPU required power of 2 textures. Even though it's quite the same thing, even modern games still pack 2D textures that are applied to 3D models.
Beyond video games sprite sheets have found yet another use on the web. A right in front of us example is the StackExchange favicon spritesheet The reason in this is similar, yet different:
- similar because it's still an optimisation
- different because we can easily load each individual icon, that would mean making multiple individual HTTP requests for every single one (initialising the connection, waiting for acknowledgement from the server, getting the data, caching the data).
It's more efficient to do a single request and easily use CSS to display sections of one image for the right icon.
Notice that spreadsheet could've been optimised further as the meta icons are grayscale version of the main sites and there is a grayscale css filter, however that might make the overall code base a little harder to read and manage and allow the flexibility of having a meta icon that isn't necessarily a grayscale copy of the original. This is illustrating the point that they're optimising the number of requests, not necessarily file size and memory allocation.
For your own game it will up to you to find this fine balance between keeping it as tightly optimised as possible versus having your code base as flexible as it can.
Back to p5.js it would be a matter of using 2 images: a loaded sprite sheet and a separate smaller image allocated to copy() sprite pixels into.
Here's a very simple example display a few frames of a Mario sprite:
Fore reference here is the code as well:
You can run it bellow:
// full spritesheetvar spriteSheet;
// a sprite sampling from sprite sheetvar mario;
// 8 frames in the spritesheetvar numSprites = 8;
// each sprite in the sheet has this bounding boxvar spriteWidth = 18;
var spriteHeight = 24;
// start framevar spriteIndex = 1;
functionsetup(){
createCanvas(150,150);
frameRate(24);
noSmooth();
noFill();
spriteSheet = loadImage("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAJAAAAAYCAYAAAAVpXQNAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyhpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNi1jMTQwIDc5LjE2MDQ1MSwgMjAxNy8wNS8wNi0wMTowODoyMSAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENDIDIwMTggKE1hY2ludG9zaCkiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6MzE1ODI1MkNDQ0MzMTFFOEJFNjA5ODI5Q0U0NzlGOEEiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6MzE1ODI1MkRDQ0MzMTFFOEJFNjA5ODI5Q0U0NzlGOEEiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDpFM0U1NkY3RkNDQTMxMUU4QkU2MDk4MjlDRTQ3OUY4QSIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDpFM0U1NkY4MENDQTMxMUU4QkU2MDk4MjlDRTQ3OUY4QSIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PszND6MAAAXWSURBVHja7JstkKMwFIDDDgK5srISWVlZWVmJRFbiDrecQyIrK5HIypWVlcjKlStxXF5IaAj5IZDe7d5sZhjKFr6+/4QH67Vti36Gfni/f1udn9cnYtQ0PHqwb9/e/lvb+N/JYYcsG0V7lWUef8yc5YIxZ0DwJOd7dxCvWxZEpkB0odsc/ZawgOMvddoUYZY6jV1f7pLHH094OyIU4e9E1rMYVsGDB/mMgwihN+/ZcrnUz4blf3WnAYNdf/n4JPswrdEabfEfcoSCAv0NxpQsveItKgNUNs3ka13I5VI/W5b/lZ0GjDiMB4xDfcZZzdxFGeBERTC6YFgnR9AFURGvyTro1xPlcqnfHNbLVEgYX1EQFijAxqkYxFKQJQy4niijGhKWC4YuOWADLmz3+Np9CckBpsbBcy0/nqaba/3msnwTRDsA8jac32cxdBkmXN8kx9E5wetKn1kOGLZZWkHwmDJeYhuQLShOzDZGmfZljILNbnT9YD9Rtzks/xkCLWXwTmpu7z2LOD6OyDH7O+xR9f4UxpQstU0OnjEIaCxTf0w/BxqZSBXEchd59NCDcSAAuX2eR4O2gkvWiwoyUozbw90G63XoBJnDEAORZ/FGFu9+VCwXDJHHl3dWzfh9hQPkYKggwOkHCxga5Hzyace5HFZRWrl629N9kpZk0+o3k+WbIJANMkhn9Wh8izqDocqMEYtWi0XyzGFYZmn9EaHtZtWt8wxBrQsWsSE5mnZpNe8rF9Wll81izGX5rgVywSC399hhhxrNZrlgTMlS2+SoZgb1iEP1IzJwFbCvYmwdNZxSPZ2tbFm/+ABaAnGtFFsnVHiaYCyYEiBrxRKrkscF4ykZj/kBlm0Jp5+KolV3x/dR0cYlZ3NRN0XwLGX5oy+wcksE4hnJ6kCyzZrBDNs05JYfWFus4KPLu0apeIusYLlguEwO0q2mjUa41Z/CAR3Ejjbf8QZOgqfNAj0qW451PdUJuh8a4x3YEpYvU/pKs7fYVLMEgnPD235wvS1jfzv1xiNKCY8IwtsR7VevKHovkKqj7YIxyFSapZdLg+rNxTrBZI86OrnUnEbS1SbTGQ4onkV42PH7XUg+76IN6Syco/dWZ++lLF93qzpHIH5AEIGDilWJLmE2mbGljtINcOAZV5DbBhp3zVMYYplnRganiwlikxziYBWf5xzDgvweVE/Z1AqOT4OgZcl6QZ+oiLZdgKed//L7DdXXBIXbor3fUmVgsyBiPjOyOB1fVIaFDRzPIOl6032HIesqaE0dSsh+UgFoZ1bK6PokrUwhcBJzlriHRwXQ5d3UV6TLrKUMVZmHz6QC4QSDxICtySoUlzutbXiZ+OlCxoEgguBp6kTeRcbHOVu3YGczh8sGBI+Sw8kGv2liGaew4ToBehSgcDx1gdgrxRsJDJMolIJHG7KOtlha9/mZyoJQCYpmJTI9snTB0E6ztDrbZHy37upkAIddDq/9dyInCFIio7Vc+bmfNbAsnR9mcEQWyGVeA0mcfzFAnqUUMXB+FoxP3N/5JtwaF+I6Bgus7HDxbAKR2cWU8arkcDlAP1R3j1TWXDCibqlAZIGk7vU22OtefY448Ld3dCN74xpIB1ZBpErRTBcNPVUpmAp4ReB3+UUlPxWohonRL1TTtA3yXB9EXOUQdZqTHEkWoVM0dJiMY1zHYblooHqo+hxMT8RPNzT5lVM4n6z36qHexFeZ/Nmer1tQEaXqzAiRK9Wdu56pFATZMVw9nIdZzbVp+XVIGuqdbmL8k4zHxymuSmlZUPsErZj1hAN2OmDbszWLgdPbVnJXrWQoEoVfhrBK2u3Hz/Z8FxDnSkGjja5RZg8XjGdlPKczXMOCaMDRtAP6tzenTo2WUyjfHyO+hPZCKK+IHnupfvA6qum9GMUrra6G53mt7Ddhqhl0hiXTTv/e8ESGjCPq1NtmaiYL5/3PL9V7X/G/MnQverMAUK1Zprx4PpXxM75pAP2M7zP+CDAA39ndLOWkvxoAAAAASUVORK5CYII=");
// create an image to draw a single sprite into
mario = createImage(spriteWidth,spriteHeight);
}
// set all pixels (R,G,B,A) to the same value (e.g. clear image with a colour)functionsetAllPixels(image,brightness){
// prep. pixels for manipulation
image.loadPixels();
let numPixels = image.pixels.length;
// loop through all pixels (spriteWidth * spriteHeight * colourChannels(4))for(let i = 0 ; i < numPixels; i++){
image.pixels[i] = brightness;
}
// commit value changes to image: updates it all in one go, more efficient than set()
image.updatePixels();
}
functiondraw(){
// clear framebackground(255);
// display the whole sprite sheetimage(spriteSheet,0,0);
// increment sprite index
spriteIndex++;
// reset sprite index if out of boundsif(spriteIndex >= numSprites){
spriteIndex = 0;
}
// visualise sprite copy rectrect(spriteIndex * spriteWidth,0,spriteWidth,spriteHeight);
// clear mario imagesetAllPixels(mario,255);
// copy pixels from sprite sheet into sprite// copy (source image, source coordinates(x,y,w,h), destination coordiantes (x,y,w,h) )
mario.copy(spriteSheet,
spriteIndex * spriteWidth,0,spriteWidth,spriteHeight,
0 ,0,spriteWidth,spriteHeight);
// display mario spriteimage(mario,mouseX,mouseY+spriteHeight);
}
<scriptsrc="https://cdnjs.cloudflare.com/ajax/libs/p5.js/0.7.3/p5.min.js"></script>
I'm using a base64 encoded string to avoid CORS issues, but you should be able to use preload()
and loadImage()
to use your own sprite sheet.
In terms of NES games I recommend you check out Writing NES Games! With Assembly!! and How we fit an NES game into 40 Kilobytes. They're both impressive technical achievements and do a very good job at visualising sprite sheet and palette limitations on the platform.
You won't have to go through these hurdles and understanding binary/bytes to be able to use in p5.js as you could see earlier, but it's interesting to understand these older constraints to build efficient games.
In terms of software, there are multiple options out there. Even though I'm not endorsed I can recommend Texture Packer. There's a simple web app version you can try online now: SpriteSheetPacker and they have a couple of silly informercial like animations: SpriteSheets - The Movie Part 1 and Sprite Sheets - The Movie Pt. 2 - Performance
Back in actionscript days there were a couple of really good pixel centred game engines: Flixel(used for the original Canabalt) and FlashPunk. There are HaXe ports available: such as HaxeFlixel and HaxePunk as well as other native JS ones (e.g. PixelJS, phaser, ImpactJS, etc.).
Recently it's been interesting to see PixelArt style games using 2D WebGL engines such as PixiJS. Although very commercial and simple in terms of game mechanics, here's a nicely rendered game by Stink Digital Studios: Miu Miu Twist
p5.js is excellent to fully understand some of the basics notions which are crucial in terms of loading/handling assets, working with pixels, handling input, etc. as is a fairly broadly encompassing library so bare in mind it might be optimised for games alone. Great way start though!
Post a Comment for "How Do You Work With Sprites, And Why Were They Used So Widely In Older Games?"