1 module PixelPerfectEngine.graphics.layers.spritelayer; 2 3 public import PixelPerfectEngine.graphics.layers.base; 4 5 import collections.treemap; 6 import collections.sortedlist; 7 import std.bitmanip : bitfields; 8 9 /** 10 * General-purpose sprite controller and renderer. 11 */ 12 public class SpriteLayer : Layer, ISpriteLayer { 13 /** 14 * Helps to determine the displaying properties and order of sprites. 15 */ 16 public struct DisplayListItem { 17 Box position; /// Stores the position relative to the origin point. Actual display position is determined by the scroll positions. 18 Box slice; /// To compensate for the lack of scanline interrupt capabilities, this enables chopping off parts of a sprite. 19 void* pixelData; /// Points to the pixel data. 20 /** 21 * From version 0.10.0 onwards, each sprites can have their own rendering function set up to 22 * allow different effect on a single layer. 23 * If not specified otherwise, the layer's main rendering function will be used instead. 24 * Custom rendering functions can be written by the user, it requires knowledge of writing 25 * pixel shader-like functions using fixed-point arithmetics. Use of vector optimizatons 26 * techniques (SSE2, AVX, NEON, etc) are needed for optimal performance. 27 */ 28 @nogc pure nothrow void function(uint* src, uint* dest, size_t length, ubyte value) renderFunc; 29 int width; /// Width of the sprite 30 int height; /// Height of the sprite 31 int scaleHoriz; /// Horizontal scaling 32 int scaleVert; /// Vertical scaling 33 int priority; /// Used for automatic sorting and identification. 34 /** 35 * Selects the palette of the sprite. 36 * Amount of accessable color depends on the palette access shifting value. A value of 8 enables 37 * 256 * 256 color palettes, and a value of 4 enables 4096 * 16 color palettes. 38 * `paletteSh` can be set lower than what the bitmap is capable of storing at its maximum, this 39 * can enable the packing of more palettes within the main one, e.g. a `paletteSh` value of 7 40 * means 512 * 128 color palettes, while the bitmaps are still stored in the 8 bit "chunky" mode 41 * instead of 7 bit planar that would require way more processing power. However this doesn't 42 * limit the bitmap's ability to access 256 colors, and this can result in memory leakage if 43 * the end developer isn't careful enough. 44 */ 45 ushort paletteSel; 46 //ubyte flags; /// Flags packed into a single byte (bitmapType, paletteSh) 47 mixin(bitfields!( 48 ubyte, "paletteSh", 4, 49 ubyte, "bmpType", 4, 50 )); 51 ubyte masterAlpha = ubyte.max;/// Sets the master alpha value of the sprite, e.g. opacity 52 //ubyte wordLength; /// Determines the word length of a sprite in a much quicker way than getting classinfo. 53 //ubyte paletteSh; /// Palette shifting value. 8 is default for 8 bit, and 4 for 4 bit bitmaps. (see paletteSel for more info) 54 //static enum ubyte PALETTESH_MASK = 0x0F; /// Mask for paletteSh 55 //static enum ubyte BMPTYPE_MASK = 0x80; /// Mask for bmpType 56 /** 57 * Creates a display list item with palette selector. 58 */ 59 this(Coordinate position, ABitmap sprite, int priority, ushort paletteSel = 0, int scaleHoriz = 1024, 60 int scaleVert = 1024) pure @trusted nothrow { 61 this.position = position; 62 this.width = sprite.width; 63 this.height = sprite.height; 64 this.priority = priority; 65 this.paletteSel = paletteSel; 66 this.scaleVert = scaleVert; 67 this.scaleHoriz = scaleHoriz; 68 slice = Coordinate(0,0,sprite.width,sprite.height); 69 if (typeid(sprite) is typeid(Bitmap4Bit)) { 70 bmpType = BitmapTypes.Bmp4Bit; 71 paletteSh = 4; 72 pixelData = (cast(Bitmap4Bit)(sprite)).getPtr; 73 } else if (typeid(sprite) is typeid(Bitmap8Bit)) { 74 bmpType = BitmapTypes.Bmp8Bit; 75 paletteSh = 8; 76 pixelData = (cast(Bitmap8Bit)(sprite)).getPtr; 77 } else if (typeid(sprite) is typeid(Bitmap16Bit)) { 78 bmpType = BitmapTypes.Bmp16Bit; 79 pixelData = (cast(Bitmap16Bit)(sprite)).getPtr; 80 } else if (typeid(sprite) is typeid(Bitmap32Bit)) { 81 bmpType = BitmapTypes.Bmp32Bit; 82 pixelData = (cast(Bitmap32Bit)(sprite)).getPtr; 83 } 84 } 85 /** 86 * Creates a display list item without palette selector. 87 */ 88 this(Coordinate position, Coordinate slice, ABitmap sprite, int priority, int scaleHoriz = 1024, 89 int scaleVert = 1024) pure @trusted nothrow { 90 if(slice.top < 0) 91 slice.top = 0; 92 if(slice.left < 0) 93 slice.left = 0; 94 if(slice.right >= sprite.width) 95 slice.right = sprite.width - 1; 96 if(slice.bottom >= sprite.height) 97 slice.bottom = sprite.height - 1; 98 this.slice = slice; 99 this(position, sprite, priority, paletteSel, scaleHoriz, scaleVert); 100 } 101 /+ 102 /// Palette shifting value. 8 is default for 8 bit, and 4 for 4 bit bitmaps. (see paletteSel for more info) 103 @property ubyte paletteSh() @safe @nogc pure nothrow const { 104 return cast(ubyte)flags & PALETTESH_MASK; 105 } 106 /// Palette shifting value. 8 is default for 8 bit, and 4 for 4 bit bitmaps. (see paletteSel for more info) 107 @property ubyte paletteSh(ubyte val) @safe @nogc pure nothrow { 108 flags &= ~PALETTESH_MASK; 109 flags |= val; 110 return cast(ubyte)flags & PALETTESH_MASK; 111 } 112 /// Defines the type of bitmap the sprite is using. This method is much faster and simpler than checking the class type of the bitmap. 113 @property BitmapTypes bmpType() @safe @nogc pure nothrow const { 114 return cast(BitmapTypes)((flags & BMPTYPE_MASK) >>> 4); 115 } 116 /// Defines the type of bitmap the sprite is using. This method is much faster and simpler than checking the class type of the bitmap. 117 @property BitmapTypes bmpType(BitmapTypes val) @safe @nogc pure nothrow { 118 flags &= ~BMPTYPE_MASK; 119 flags |= cast(ubyte)val << 4; 120 return bmpType; 121 }+/ 122 /** 123 * Resets the slice to its original position. 124 */ 125 void resetSlice() pure @nogc @safe nothrow { 126 slice.left = 0; 127 slice.top = 0; 128 slice.right = position.width - 1; 129 slice.bottom = position.height - 1; 130 } 131 /** 132 * Replaces the sprite with a new one. 133 * If the sizes are mismatching, the top-left coordinates are left as is, but the slicing is reset. 134 */ 135 void replaceSprite(ABitmap sprite) @trusted pure nothrow { 136 //this.sprite = sprite; 137 //palette = sprite.getPalettePtr(); 138 if(this.width != sprite.width || this.height != sprite.height){ 139 this.width = sprite.width; 140 this.height = sprite.height; 141 position.right = position.left + cast(int)scaleNearestLength(width, scaleHoriz); 142 position.bottom = position.top + cast(int)scaleNearestLength(height, scaleVert); 143 resetSlice(); 144 } 145 if (typeid(sprite) is typeid(Bitmap4Bit)) { 146 bmpType = BitmapTypes.Bmp4Bit; 147 paletteSh = 4; 148 pixelData = (cast(Bitmap4Bit)(sprite)).getPtr; 149 } else if (typeid(sprite) is typeid(Bitmap8Bit)) { 150 bmpType = BitmapTypes.Bmp8Bit; 151 paletteSh = 8; 152 pixelData = (cast(Bitmap8Bit)(sprite)).getPtr; 153 } else if (typeid(sprite) is typeid(Bitmap16Bit)) { 154 bmpType = BitmapTypes.Bmp16Bit; 155 pixelData = (cast(Bitmap16Bit)(sprite)).getPtr; 156 } else if (typeid(sprite) is typeid(Bitmap32Bit)) { 157 bmpType = BitmapTypes.Bmp32Bit; 158 pixelData = (cast(Bitmap32Bit)(sprite)).getPtr; 159 } 160 } 161 @nogc int opCmp(in DisplayListItem d) const pure @safe nothrow { 162 return priority - d.priority; 163 } 164 @nogc bool opEquals(in DisplayListItem d) const pure @safe nothrow { 165 return priority == d.priority; 166 } 167 @nogc int opCmp(in int pri) const pure @safe nothrow { 168 return priority - pri; 169 } 170 @nogc bool opEquals(in int pri) const pure @safe nothrow { 171 return priority == pri; 172 } 173 174 string toString() const { 175 import std.conv : to; 176 return "{Position: " ~ position.toString ~ ";\nDisplayed portion: " ~ slice.toString ~";\nPriority: " ~ 177 to!string(priority) ~ "; PixelData: " ~ to!string(pixelData) ~ 178 "; PaletteSel: " ~ to!string(paletteSel) ~ "; bmpType: " ~ to!string(bmpType) ~ "}"; 179 } 180 } 181 alias DisplayList = TreeMap!(int, DisplayListItem); 182 alias OnScreenList = SortedList!(int, "a < b", false); 183 //protected DisplayListItem[] displayList; ///Stores the display data 184 protected DisplayList allSprites; ///All sprites of this layer 185 protected OnScreenList displayedSprites; ///Sprites that are being displayed 186 protected Color[2048] src; ///Local buffer for scaling 187 //size_t[8] prevSize; 188 ///Default ctor 189 public this(RenderingMode renderMode = RenderingMode.AlphaBlend) @nogc nothrow @safe { 190 setRenderingMode(renderMode); 191 //src[0].length = 1024; 192 } 193 /** 194 * Checks all sprites for whether they're on screen or not. 195 * Called every time the layer is being scrolled. 196 */ 197 public void checkAllSprites() @safe pure nothrow { 198 foreach (key; allSprites) { 199 checkSprite(key); 200 } 201 } 202 /** 203 * Checks whether a sprite would be displayed on the screen, then updates the display list. 204 * Returns true if it's on screen. 205 */ 206 public bool checkSprite(int n) @safe pure nothrow { 207 return checkSprite(allSprites[n]); 208 } 209 ///Ditto. 210 protected bool checkSprite(DisplayListItem sprt) @safe pure nothrow { 211 //assert(sprt.bmpType != BitmapTypes.Undefined && sprt.pixelData, "DisplayList error!"); 212 if(sprt.slice.width && sprt.slice.height 213 && (sprt.position.right > sX && sprt.position.bottom > sY && 214 sprt.position.left < sX + rasterX && sprt.position.top < sY + rasterY)) { 215 displayedSprites.put(sprt.priority); 216 return true; 217 } else { 218 displayedSprites.removeByElem(sprt.priority); 219 return false; 220 } 221 } 222 /** 223 * Searches the DisplayListItem by priority and returns it. 224 * Can be used for external use without any safety issues. 225 */ 226 public DisplayListItem getDisplayListItem(int n) @nogc pure @safe nothrow { 227 return allSprites[n]; 228 } 229 /** 230 * Searches the DisplayListItem by priority and returns it. 231 * Intended for internal use, as it returns it as a reference value. 232 */ 233 protected DisplayListItem* getDisplayListItem_internal(int n) @nogc pure @safe nothrow { 234 return allSprites.ptrOf(n); 235 } 236 override public void setRasterizer(int rX,int rY) { 237 super.setRasterizer(rX,rY); 238 } 239 ///Returns the displayed portion of the sprite. 240 public Coordinate getSlice(int n) @nogc pure @safe nothrow { 241 return getDisplayListItem(n).slice; 242 } 243 ///Writes the displayed portion of the sprite. 244 ///Returns the new slice, if invalid (greater than the bitmap, etc.) returns Coordinate.init. 245 public Coordinate setSlice(int n, Coordinate slice) @safe pure nothrow { 246 DisplayListItem* sprt = allSprites.ptrOf(n); 247 if(sprt) { 248 sprt.slice = slice; 249 checkSprite(*sprt); 250 return sprt.slice; 251 } else { 252 return Coordinate.init; 253 } 254 } 255 ///Returns the selected paletteID of the sprite. 256 public ushort getPaletteID(int n) @nogc pure @safe nothrow { 257 return getDisplayListItem(n).paletteSel; 258 } 259 ///Sets the paletteID of the sprite. Returns the new ID, which is truncated to the possible values with a simple binary and operation 260 ///Palette must exist in the parent Raster, otherwise AccessError might happen 261 public ushort setPaletteID(int n, ushort paletteID) @nogc pure @safe nothrow { 262 return getDisplayListItem_internal(n).paletteSel = paletteID; 263 } 264 /** 265 * Adds a sprite to the layer. 266 */ 267 public void addSprite(ABitmap s, int n, Box c, ushort paletteSel = 0, int scaleHoriz = 1024, 268 int scaleVert = 1024) @safe nothrow { 269 DisplayListItem d = DisplayListItem(c, s, n, paletteSel, scaleHoriz, scaleVert); 270 d.renderFunc = mainRenderingFunction; 271 synchronized 272 allSprites[n] = d; 273 checkSprite(d); 274 } 275 ///Ditto 276 public void addSprite(ABitmap s, int n, int x, int y, ushort paletteSel = 0, int scaleHoriz = 1024, 277 int scaleVert = 1024) @safe nothrow { 278 DisplayListItem d = DisplayListItem(Box(x, y, s.width + x, s.height + y), s, n, paletteSel, scaleHoriz, scaleVert); 279 d.renderFunc = mainRenderingFunction; 280 synchronized 281 allSprites[n] = d; 282 checkSprite(d); 283 } 284 /** 285 * Replaces the bitmap of the given sprite. 286 */ 287 public void replaceSprite(ABitmap s, int n) @safe nothrow { 288 DisplayListItem* sprt = getDisplayListItem_internal(n); 289 sprt.replaceSprite(s); 290 checkSprite(*sprt); 291 } 292 ///Ditto with move 293 public void replaceSprite(ABitmap s, int n, int x, int y) @safe nothrow { 294 DisplayListItem* sprt = getDisplayListItem_internal(n); 295 sprt.replaceSprite(s); 296 sprt.position.move(x, y); 297 checkSprite(*sprt); 298 } 299 ///Ditto with move 300 public void replaceSprite(ABitmap s, int n, Coordinate c) @safe nothrow { 301 DisplayListItem* sprt = allSprites.ptrOf(n); 302 sprt.replaceSprite(s); 303 sprt.position = c; 304 checkSprite(*sprt); 305 } 306 /** 307 * Removes a sprite from both displaylists by priority. 308 */ 309 public void removeSprite(int n) @safe nothrow { 310 synchronized { 311 displayedSprites.removeByElem(n); 312 allSprites.remove(n); 313 } 314 } 315 ///Clears all sprite from the layer. 316 public void clear() @safe nothrow { 317 displayedSprites = OnScreenList.init; 318 allSprites = DisplayList.init; 319 } 320 /** 321 * Moves a sprite to the given position. 322 */ 323 public void moveSprite(int n, int x, int y) @safe nothrow { 324 DisplayListItem* sprt = allSprites.ptrOf(n); 325 sprt.position.move(x, y); 326 checkSprite(*sprt); 327 } 328 /** 329 * Moves a sprite by the given amount. 330 */ 331 public void relMoveSprite(int n, int x, int y) @safe nothrow { 332 DisplayListItem* sprt = allSprites.ptrOf(n); 333 sprt.position.relMove(x, y); 334 checkSprite(*sprt); 335 } 336 ///Sets the rendering function for the sprite (defaults to the layer's rendering function) 337 public void setSpriteRenderingMode(int n, RenderingMode mode) @safe nothrow { 338 DisplayListItem* sprt = allSprites.ptrOf(n); 339 sprt.renderFunc = getRenderingFunc(mode); 340 } 341 public @nogc Coordinate getSpriteCoordinate(int n) @safe nothrow { 342 return allSprites[n].position; 343 } 344 ///Scales sprite horizontally. Returns the new size, or -1 if the scaling value is invalid, or -2 if spriteID not found. 345 public int scaleSpriteHoriz(int n, int hScl) @trusted nothrow { 346 DisplayListItem* sprt = allSprites.ptrOf(n); 347 if(!sprt) return -2; 348 else if(!hScl) return -1; 349 else { 350 sprt.scaleHoriz = hScl; 351 const int newWidth = cast(int)scaleNearestLength(sprt.width, hScl); 352 sprt.slice.right = newWidth; 353 sprt.position.right = sprt.position.left + newWidth; 354 checkSprite(*sprt); 355 return newWidth; 356 } 357 } 358 ///Scales sprite vertically. Returns the new size, or -1 if the scaling value is invalid, or -2 if spriteID not found. 359 public int scaleSpriteVert(int n, int vScl) @trusted nothrow { 360 DisplayListItem* sprt = allSprites.ptrOf(n); 361 if(!sprt) return -2; 362 else if(!vScl) return -1; 363 else { 364 sprt.scaleVert = vScl; 365 const int newHeight = cast(int)scaleNearestLength(sprt.height, vScl); 366 sprt.slice.bottom = newHeight; 367 sprt.position.bottom = sprt.position.top + newHeight; 368 checkSprite(*sprt); 369 return newHeight; 370 } 371 /+if (!vScl) return -1; 372 for(int i; i < displayList.length ; i++){ 373 if(displayList[i].priority == n){ 374 displayList[i].scaleVert = vScl; 375 const int newHeight = cast(int)scaleNearestLength(displayList[i].height, vScl); 376 displayList[i].slice.bottom = newHeight; 377 return displayList[i].position.bottom = displayList[i].position.top + newHeight; 378 } 379 } 380 return -2;+/ 381 } 382 ///Gets the sprite's current horizontal scale value 383 public int getScaleSpriteHoriz(int n) @nogc @trusted nothrow { 384 return allSprites[n].scaleHoriz; 385 } 386 ///Gets the sprite's current vertical scale value 387 public int getScaleSpriteVert(int n) @nogc @trusted nothrow { 388 return allSprites[n].scaleVert; 389 } 390 public override @nogc void updateRaster(void* workpad, int pitch, Color* palette) { 391 /* 392 * BUG 1: If sprite is wider than 2048 pixels, it'll cause issues (mostly memory leaks) due to a hack. 393 * BUG 2: Obscuring the top part of a sprite when scaleVert is not 1024 will cause glitches. 394 */ 395 foreach (priority ; displayedSprites) { 396 //foreach(i ; displayList){ 397 DisplayListItem i = allSprites[priority]; 398 const int left = i.position.left + i.slice.left; 399 const int top = i.position.top + i.slice.top; 400 const int right = i.position.left + i.slice.right; 401 const int bottom = i.position.top + i.slice.bottom; 402 /+if((i.position.right > sX && i.position.bottom > sY) && (i.position.left < sX + rasterX && i.position.top < sY + 403 rasterY)){+/ 404 //if((right > sX && left < sX + rasterX) && (bottom > sY && top < sY + rasterY) && i.slice.width && i.slice.height){ 405 int offsetXA = sX > left ? sX - left : 0;//Left hand side offset, zero if not obscured 406 const int offsetXB = sX + rasterX < right ? right - (sX + rasterX) : 0; //Right hand side offset, zero if not obscured 407 const int offsetYA = sY > top ? sY - top : 0; //top offset of sprite, zero if not obscured 408 const int offsetYB = sY + rasterY < bottom ? bottom - (sY + rasterY) + 1 : 1; //bottom offset of sprite, zero if not obscured 409 //const int offsetYB0 = cast(int)scaleNearestLength(offsetYB, i.scaleVert); 410 const int sizeX = i.slice.width(); //total displayed width 411 const int offsetX = left - sX; 412 const int length = sizeX - offsetXA - offsetXB - 1; 413 //int lengthY = i.slice.height - offsetYA - offsetYB; 414 //const int lfour = length * 4; 415 const int offsetY = sY < top ? (top-sY)*pitch : 0; //used if top portion of the sprite is off-screen 416 //offset = i.scaleVert % 1024; 417 const int scaleVertAbs = i.scaleVert * (i.scaleVert < 0 ? -1 : 1); //absolute value of vertical scaling, used in various calculations 418 //int offset, prevOffset; 419 const int offsetAmount = scaleVertAbs <= 1024 ? 1024 : scaleVertAbs; //used to limit the amount of re-rendering every line 420 //offset = offsetYA<<10; 421 const int offsetYA0 = cast(int)(cast(double)offsetYA / (1024.0 / cast(double)scaleVertAbs)); //amount of skipped lines (I think) TODO: remove floating-point arithmetic 422 const int sizeXOffset = i.width * (i.scaleVert < 0 ? -1 : 1); 423 int prevOffset = offsetYA0 * offsetAmount; // 424 int offset = offsetYA0 * scaleVertAbs; 425 const size_t p0offset = (i.scaleHoriz > 0 ? offsetXA : offsetXB); //determines offset based on mirroring 426 // HACK: as I couldn't figure out a better method yet I decided to scale a whole line, which has a lot of problems 427 const int scalelength = i.position.width < 2048 ? i.width : 2048; //limit width to 2048, the minimum required for this scaling method to work 428 void* dest = workpad + (offsetX + offsetXA)*4 + offsetY; 429 final switch (i.bmpType) with (BitmapTypes) { 430 case Bmp4Bit: 431 ubyte* p0 = cast(ubyte*)i.pixelData + i.width * ((i.scaleVert < 0 ? (i.height - offsetYA0 - 1) : offsetYA0)>>1); 432 for(int y = offsetYA ; y < i.slice.height - offsetYB ; ){ 433 horizontalScaleNearest4BitAndCLU(p0, src.ptr, palette + (i.paletteSel<<i.paletteSh), scalelength, offsetXA & 1, 434 i.scaleHoriz); 435 prevOffset += offsetAmount; 436 for(; offset < prevOffset; offset += scaleVertAbs){ 437 y++; 438 i.renderFunc(cast(uint*)src.ptr + p0offset, cast(uint*)dest, length, i.masterAlpha); 439 dest += pitch; 440 } 441 p0 += sizeXOffset >> 1; 442 } 443 //} 444 break; 445 case Bmp8Bit: 446 ubyte* p0 = cast(ubyte*)i.pixelData + i.width * (i.scaleVert < 0 ? (i.height - offsetYA0 - 1) : offsetYA0); 447 for(int y = offsetYA ; y < i.slice.height - offsetYB ; ){ 448 horizontalScaleNearestAndCLU(p0, src.ptr, palette + (i.paletteSel<<i.paletteSh), scalelength, i.scaleHoriz); 449 prevOffset += 1024; 450 for(; offset < prevOffset; offset += scaleVertAbs){ 451 y++; 452 i.renderFunc(cast(uint*)src.ptr + p0offset, cast(uint*)dest, length, i.masterAlpha); 453 dest += pitch; 454 } 455 p0 += sizeXOffset; 456 } 457 break; 458 case Bmp16Bit: 459 ushort* p0 = cast(ushort*)i.pixelData + i.width * (i.scaleVert < 0 ? (i.height - offsetYA0 - 1) : offsetYA0); 460 for(int y = offsetYA ; y < i.slice.height - offsetYB ; ){ 461 horizontalScaleNearestAndCLU(p0, src.ptr, palette, scalelength, i.scaleHoriz); 462 prevOffset += 1024; 463 for(; offset < prevOffset; offset += scaleVertAbs){ 464 y++; 465 i.renderFunc(cast(uint*)src.ptr + p0offset, cast(uint*)dest, length, i.masterAlpha); 466 dest += pitch; 467 } 468 p0 += sizeXOffset; 469 } 470 break; 471 case Bmp32Bit: 472 Color* p0 = cast(Color*)i.pixelData + i.width * (i.scaleVert < 0 ? (i.height - offsetYA0 - 1) : offsetYA0); 473 for(int y = offsetYA ; y < i.slice.height - offsetYB ; ){ 474 horizontalScaleNearest(p0, src.ptr, scalelength, i.scaleHoriz); 475 prevOffset += 1024; 476 for(; offset < prevOffset; offset += scaleVertAbs){ 477 y++; 478 i.renderFunc(cast(uint*)src.ptr + p0offset, cast(uint*)dest, length, i.masterAlpha); 479 dest += pitch; 480 } 481 p0 += sizeXOffset; 482 } 483 //} 484 break; 485 case Undefined, Bmp1Bit, Bmp2Bit, Planar: 486 break; 487 } 488 489 //} 490 } 491 //foreach(int threadOffset; threads.parallel) 492 //free(src[threadOffset]); 493 } 494 ///Absolute scrolling. 495 public override void scroll(int x, int y) @safe pure nothrow { 496 sX = x; 497 sY = y; 498 checkAllSprites; 499 } 500 ///Relative scrolling. Positive values scrolls the layer left and up, negative values scrolls the layer down and right. 501 public override void relScroll(int x, int y) @safe pure nothrow { 502 sX += x; 503 sY += y; 504 checkAllSprites; 505 } 506 }