【问题标题】:How do you work with sprites, and why were they used so widely in older games?你是如何使用精灵的,为什么它们在旧游戏中被如此广泛地使用?
【发布时间】:2019-04-08 01:20:10
【问题描述】:

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

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

我查看了 p5.js 库具有 sprite 功能,但是当我四处寻找它们的工作原理时,它们总是只用于制作彩色方块,而没有展示如何将它们与存储所有图像的图像文件一起使用。

我的问题是:

  • 根据定义,精灵是否具有执行所有操作(例如更改精灵的调色板、将其向后或上下颠倒等)的能力?
  • 与使用图像显示功能和仅使用 png 相比,使用 sprite 有什么优势吗?
  • 为什么它们在较旧的硬件中得到如此广泛的应用,而如今它们仍在现代复古风格的游戏(Showelknight、Dead Cells 等)中使用?

【问题讨论】:

    标签: javascript colors sprite p5.js


    【解决方案1】:

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

    • 根据定义,精灵是否具有执行所有操作的能力,例如更改精灵的调色板、将其向后或上下颠倒等?

      不,您仍然必须手动对其进行编程。 (NES 有辅助指令,p5.js 目前没有作为 p5.Image AFAIK 一部分的翻转/旋转 90 度功能,但是您可以“作弊”并使用PGraphics 缓冲区来绘制应用转换(translate()/rotate()/scale()实现翻转和旋转)

    • 与使用图像显示功能和仅使用 png 相比,使用 sprite 有什么优势吗?

      您可以为精灵表分配内存一次,然后简单地引用该区域,因为稍后需要复制帧(与数组中的许多独立图像相反,需要多次加载/解码资产) .每个角色/游戏对象拥有更多帧,并且能够有效打包像素的更多游戏对象真正节省了 RAM,让您可以将其用于更有趣的游戏机制和效果,而不仅仅是原始资产。

    • 为什么它们在较旧的硬件中如此广泛地使用,并且今天仍在现代复古风格的游戏(Showelknight、Dead Cells 等)中使用?

    当时是硬件的限制,因此必须尽可能地节省资产,以便能够通过严格的控制/游戏机制和故事来吸引观众。它们今天仍然用于 3D 视频游戏和实时图形:GPU 需要 2 个纹理的能力。即使是完全一样的东西,即使是现代游戏仍然包含应用于 3D 模型的 2D 纹理。

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

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

    执行单个请求并轻松使用 CSS 为右侧图标显示一个图像的各个部分会更有效。

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

    对于您自己的游戏,您需要在尽可能严格优化与尽可能灵活的代码库之间找到这种良好平衡。

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

    Here 是一个非常简单的示例,显示几帧马里奥精灵:

    这里的前参考也是代码:

    你可以在下面运行它:

    // 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);
    }
    &lt;script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/0.7.3/p5.min.js"&gt;&lt;/script&gt;

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

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

    您不必经历这些障碍并理解二进制/字节即可在 p5.js 中使用,如您之前所见,但了解这些旧约束以构建高效游戏很有趣。

    在软件方面,有多种选择。 即使我不被认可,我也可以推荐 Texture Packer。 有一个简单的网络应用程序版本,你现在可以在网上试用:SpriteSheetPacker,他们有几个愚蠢的非正式动画:SpriteSheets - The Movie Part 1Sprite Sheets - The Movie Pt. 2 - Performance

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

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

    p5.js 非常适合完全理解一些在加载/处理资产、处理像素、处理输入等方面至关重要的基本概念。 由于是一个包含相当广泛的库,因此考虑到它可能仅针对游戏进行优化。不错的开始方式!

    【讨论】:

    • 您好,非常感谢您的帮助。你的消息来源真的很有帮助,希望我能从现在开始自己弄清楚。没想到直接用p5js库得到帮助,但我猜它比我想象的更受欢迎。希望你有一个美好的一天!
    猜你喜欢
    • 2017-03-12
    • 2013-09-06
    • 1970-01-01
    • 2011-02-01
    • 1970-01-01
    • 2020-08-30
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多