你如何使用精灵,为什么他们在旧游戏中如此广泛地使用?

问题描述 投票:1回答:1

我和我的朋友决定一起创建一个平台游戏作为我们的计算机科学项目。我目前正在使用p5.js库中的图像功能来交换图像并为步行,跳跃等创建动画。我查看了之前在Sega Mega Drive和NES等系统上进行2D游戏的过程,并了解到当时几乎所有用于2D空间中的图形的都是精灵。

经过更多的研究,我发现精灵被制作成一个大的图像文件,上面有很多不同的帧动画,像NES这样的系统甚至可以向后和向下翻转这些图像部分!我甚至读过你可以将同一个精灵重新着色到不同的调色板!

我查了一下p5.js库有sprite功能,但是当我四处寻找它们的工作方式时,它们总是只用于制作彩色方块而不显示如何将它们用于存储所有图像的图像文件。

我的问题是这些:

  • 根据定义精灵是否有能力做所有事情,比如更改精灵的调色板,向后或向上翻转等等?
  • 与使用图像显示功能和使用png相比,使用精灵有什么好处吗?
  • 为什么它们在旧硬件中被如此广泛地使用,并且它们今天仍然用于现代复古风格的游戏(Showelknight,Dead Cells等)?
javascript colors sprite p5.js
1个回答
2
投票

要简单地回答你的精灵表(也称为纹理地图集)问题:

  • 根据定义精灵是否有能力做所有事情,比如更改精灵的调色板,向后或向上翻转等等? 不,你必须手动编程那个。 (NES有辅助说明,p5.js目前没有翻转/旋转90度功能作为p5.Image AFAIK的一部分,但是你可以“作弊”并使用PGraphics缓冲区来绘制应用变换(translate()/rotate()/scale()来实现翻转和回转)
  • 与使用图像显示功能和使用png相比,使用精灵有什么好处吗? 您将为精灵表分配一次内存,然后简单地引用稍后需要复制帧的区域(与阵列中的许多独立图像相反,多次加载/解码资产)。每个角色/游戏对象拥有更多帧,并且更多游戏对象能够有效地打包像素,真正节省RAM可以让你使用它来获得更有趣的游戏机制和效果,而不仅仅是原始资产。
  • 为什么它们在旧硬件中被如此广泛地使用,并且它们今天仍然用于现代复古风格的游戏(Showelknight,Dead Cells等)?

当时它是硬件的约束因此,尽可能节省资产以通过严格的控制/游戏机制和故事捕捉观众是至关重要的。它们至今仍用于3D视频游戏和实时图形:GPU需要2个纹理的力量。即使它是完全相同的东西,即使现代游戏仍然包装应用于3D模型的2D纹理。

除了视频游戏之外,精灵表还发现了网络上的另一种用途。在我们面前的一个例子是StackExchange favicon spritesheet这个原因是相似但不同的:

  • 类似,因为它仍然是一个优化
  • 不同之处在于我们可以轻松加载每个单独的图标,这意味着为每个图标创建多个单独的HTTP请求(初始化连接,等待来自服务器的确认,获取数据,缓存数据)。

执行单个请求并使用CSS显示正确图标的一个图像的部分更有效。

请注意,电子表格可以进一步优化,因为元图标是主要网站的灰度版本,并且有一个灰度级css过滤器,但这可能会使整个代码库更难以阅读和管理,并允许具有元图标不一定是原始的灰度副本。这表明他们正在优化请求数量,而不一定是文件大小和内存分配。

对于您自己的游戏,您可以在尽可能保持最佳优化与尽可能灵活的代码库之间找到这种精细平衡。

回到p5.js,这将是一个使用2个图像的问题:一个加载的精灵表和分配给copy()精灵像素的单独的较小图像。

Here是一个非常简单的例子,显示了一些Mario精灵的帧:

这里的前面引用也是代码:

mario spritesheet

你可以运行它:

// full spritesheet
var spriteSheet;
// a sprite sampling from sprite sheet
var mario;

// 8 frames in the spritesheet
var numSprites   = 8;
// each sprite in the sheet has this bounding box
var spriteWidth  = 18;
var spriteHeight = 24; 
// start frame
var spriteIndex  = 1;

function setup(){
  createCanvas(150,150);
  frameRate(24);
  noSmooth();
  noFill();
  spriteSheet = loadImage("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAJAAAAAYCAYAAAAVpXQNAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyhpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNi1jMTQwIDc5LjE2MDQ1MSwgMjAxNy8wNS8wNi0wMTowODoyMSAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENDIDIwMTggKE1hY2ludG9zaCkiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6MzE1ODI1MkNDQ0MzMTFFOEJFNjA5ODI5Q0U0NzlGOEEiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6MzE1ODI1MkRDQ0MzMTFFOEJFNjA5ODI5Q0U0NzlGOEEiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDpFM0U1NkY3RkNDQTMxMUU4QkU2MDk4MjlDRTQ3OUY4QSIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDpFM0U1NkY4MENDQTMxMUU4QkU2MDk4MjlDRTQ3OUY4QSIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PszND6MAAAXWSURBVHja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");
  // 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)
function setAllPixels(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();
}

function draw(){
  // clear frame
  background(255);
  // display the whole sprite sheet
  image(spriteSheet,0,0);
  // increment sprite index
  spriteIndex++;
  // reset sprite index if out of bounds
  if(spriteIndex >= numSprites){
    spriteIndex = 0;
  }
  // visualise sprite copy rect
  rect(spriteIndex * spriteWidth,0,spriteWidth,spriteHeight);
  
  // clear mario image
  setAllPixels(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 sprite
  image(mario,mouseX,mouseY+spriteHeight);
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/0.7.3/p5.min.js"></script>

我使用base64编码的字符串来避免CORS问题,但你应该能够使用preload() and loadImage()来使用你自己的精灵表。

在NES游戏方面,我建议你看看Writing NES Games! With Assembly!!How we fit an NES game into 40 Kilobytes。它们都是令人印象深刻的技术成就,并且在平台上可视化精灵表和调色板限制方面做得非常出色。

40KB NES Game youtube video screenshot 1

40KB NES Game youtube video screenshot 2

40KB NES Game youtube video screenshot 3

40KB NES Game youtube video screenshot 4

40KB NES Game youtube video screenshot 5

40KB NES Game youtube video screenshot 6

您将不必经历这些障碍并理解二进制/字节以便能够在前面看到的p5.js中使用,但理解这些旧约束以构建高效游戏很有趣。

在软件方面,有多种选择。即使我没有得到认可,我也可以推荐Texture Packer。有一个简单的网络应用程序版本你可以在网上尝试:SpriteSheetPacker和他们有一些愚蠢的informercial像动画:SpriteSheets - The Movie Part 1Sprite Sheets - The Movie Pt. 2 - Performance

回到actionscript时代,有几个非常好的以像素为中心的游戏引擎:Flixel(用于原始的Canabalt)和FlashPunk。有可用的HaXe端口:例如HaxeFlixelHaxePunk以及其他本地JS端口(例如PixelJSphaserImpactJS等)。

Canabalt

最近看到使用像PixiJS这样的2D WebGL引擎的PixelArt风格游戏很有意思。虽然在游戏机制方面非常商业化和简单,但这是Stink Digital Studios: Miu Miu Twist精心渲染的游戏

miu miu twist preview 1

miu miu twist preview 2

miu miu twist preview 3

p5.j​​s非常适合完全理解一些基本概念,这些概念在加载/处理资产,处理像素,处理输入等方面至关重要,因为它是一个相当广泛的库,所以在考虑它时可能会针对游戏进行优化单独。好的方式开始!

© www.soinside.com 2019 - 2024. All rights reserved.