• Content Count

  • Joined

  • Last visited

  • Days Won


timetocode last won the day on November 11 2019

timetocode had the most liked content!


About timetocode

  • Rank
    Advanced Member

Contact Methods

  • Website URL
  • Twitter

Profile Information

  • Gender
    Not Telling
  • Location
    Seattle, WA
  • Interests
    game development, pixel art, procedural content generation, game music

Recent Profile Visitors

2107 profile views
  1. I've found the specific piece of state that is "shared" between multiple spine instances and thus is resistant to having its texture changed directly. You will probably not be surprised to hear that this shared state is the "Skin" because that's pretty much how it is supposed to work. Interestingly `hackTextureBySlotName` is immune to this problem because it never messed with the skin itself, it just changed the texture of one active slot of a spine instance. Skins are only involved when a spine entity is first created, when it has its skin programmatically changed, or when an animation changes the texture of an attachment. Meanwhile `hackAttachmentTexture` is essentially a skin hack -- it changes the skin. In order to use the same skeleton+animations over and over again but with different skins which are textured via pixi instead of spine atlases, the association of one skin:skeleton has to be broken. I was able to accomplish this just by cloning the skin object before creating my spine entity -- that way if I change the skin on it, i'm not changing the skin of everything else that uses that skeleton. I'm not sure if anyone would want this to be honest. I don't think that it is worth a PR or if it is then my recommending is just to add a .clone method to Skin that creates a new skin, attachments, and textures (those are the parts that are currently shared mutably). I mean it's a great feature but it simply isn't how spine works, it's basically replacing the concept of spine skins with a do-whatever-you-want-skins via pixi textures. It's hard for me to imagine any significant number of people learning spine, thinking it's awesome, but then only disliking the way skins are done and trying to hack that. I guess I was lead down that path so maybe someone will want this feature. In any case below is my incredibly hacky code that performs a partial clone of `spineData` (the thing that loads when you load a spine file) and clones all of the textures in the skin so they can be changed programmatically. // copies clone spineData loaded by pixi // .. but creates a unique instance of its skin + attachments + regions + textures // leaves bones, animations, etc all shared and mutable as before const cloneSpineData = (spineData) => { const { animations, bones, defaultSkin, events, fps, hash, height, ikConstraints, imagesPath, pathConstraints, skins, slots, transformConstraints, version, width, x, y } = spineData const copy = { animations, bones, defaultSkin, events, fps, hash, height, ikConstraints, imagesPath, pathConstraints, skins, slots, transformConstraints, version, width, x, y } // ^ this could just be one Object.assign, but I left it unfolded // in case anyone wants to clone anything else about spineData (animations? bones?) const newSkin = { attachments: [], bones: [], constraunts: [], name: 'default' } Object.setPrototypeOf(newSkin, defaultSkin.__proto__) defaultSkin.attachments.forEach(attachment => { const newAttachment = {} for (let prop in attachment) { // Attachment is an object like { 'head': RegionAttachment } // RegionAttachment const regionAttachment = attachment[prop] const newRegionAttachment = {} Object.setPrototypeOf(newRegionAttachment, regionAttachment.__proto__) Object.assign(newRegionAttachment, regionAttachment) // objects of value therein... // color newRegionAttachment.color = cloneObject(regionAttachment.color) newRegionAttachment.tempColor = cloneObject(regionAttachment.tempColor) newRegionAttachment.offset = cloneArray(regionAttachment.offset) newRegionAttachment.uvs = cloneArray(regionAttachment.uvs) // region object const newRegion = cloneObject(regionAttachment.region) // this is the most important item -- give this skin's pieces a unique texture newRegion.texture = regionAttachment.region.texture.clone() newRegionAttachment.region = newRegion // skipped: region.page (did not seem to be used in rendering) newAttachment[prop] = newRegionAttachment } newSkin.attachments.push(newAttachment) const attachmentName = Object.keys(attachment)[0] const attachmentObject = attachment[attachmentName] Object.setPrototypeOf(newAttachment, attachmentObject.__proto__) }) // and here use our new skin instance instead of a globalish skin copy.defaultSkin = newSkin copy.skins = [newSkin] Object.setPrototypeOf(copy, spineData.__proto__) return copy } // usage: don't want two Spines to share the same skin when you change it programmatically? do this const spineData = /* spine data you loaded */ const spineA = new Spine(cloneSpineData(spineData)) const spineB = new Spine(cloneSpineData(spineData)) // now spineA and spineB have the same-looking skin, but if you // change any part of their skin it only affects one of them I apologize for the hacks. Objects from the classes Skin, RegionAttachment, Region, etc are created via copying their prototypes (they're not public and I couldn't figure out how to rebuild pixi-spine). The most important class to create a copy of was Texture, and for that pixi's good ol' texture.clone() method was used. Here's a gif of a knight with randomly changing weapons+armor pieces, and also skeletons. They all are using the same humanoid.spine skeleton but with different skins that are applied via pixi. Prior to this feature changing parts of the knight would sometimes change parts of the skeletons.
  2. I have a bug where I can create two different spine instances and hacking their attachments is linked. In other words if I change the attachment in spineA the attachment in spineB may change as well. I say "may" because it doesn't always happen. I think it follows the same rules discussed above where during an animation with multiple attachments some methods affect only the active slot whereas others affect the textures associated with a skeleton. My guess is the bug is one of two things: my code above that changes the attachment texture of the skeleton is using the wrong approach when two `new Spine(spineData)` are created from the same spine data some of this state is shared instead of cloned into the spine instances (so two different Spine share the same skeleton, not just conceptually but literally in a state-ful manner) If it is #1 I'm not sure, I guess I'll find another angle. If it is #2 then perhaps adding a function like skeleton.clone() or slot.clone() or treating the spineData immutably on initial construction of the spine object (copying the slots maybe) might fix the bug. Does anyone have any idea? I also volunteer to figure this out if someone can explain to me how to build/test pixi-spine . All of my code is using things like `import { Sprite, spine } from 'pixi.js'` but i'm guessing es6 is not the way to do this? To get pixi spine running I have some weird code: import * as PIXI from "pixi.js" window.PIXI = PIXI import "pixi-spine" /*... normal es6 code from here on, though warnings appear about 'spine' not being exported by pixi */ Edit: If I use yarn build in pixi-spine, and then change my import from `import "pixi-spine"` to the local folder in which i've checked out spine and built it, I end up with an error of pixi not being found presumably from pixi-spine.
  3. I rewrote it in typescript and put it into a PR: https://github.com/pixijs/pixi-spine/pull/347 I'm sorry that I don't actually know how to build pixi, so I was unable to test the final version, you may want to make sure it works. The version of it right before I pasted it back into Spine.ts was correctly able to change attachments in my own game. For reference for anyone reading this thread for attachment-hacking related purposes, here's the added code which contains the logic from earlier but refactored to more closely follow the style of Spine.ts' hackTextureBySlotName/index /** * Changes texture of an attachment * * PIXI runtime feature, it was made to satisfy our users. * * @param slotName {string} * @param attachmentName {string} * @param [texture = null] {PIXI.Texture} If null, take default (original) texture * @param [size = null] {PIXI.Point} sometimes we need new size for region attachment, you can pass 'texture.orig' there * @returns {boolean} Success flag */ hackTextureAttachment(slotName: string, attachmentName: string, texture, size: PIXI.Rectangle = null) { // changes the texture of an attachment at the skeleton level const slotIndex = this.skeleton.findSlotIndex(slotName) const attachment: any = this.skeleton.getAttachmentByName(slotName, attachmentName) attachment.region.texture = texture const slot = this.skeleton.slots[slotIndex] if (!slot) { return false } // gets the currently active attachment in this slot const currentAttachment: any = slot.getAttachment() if (attachmentName === currentAttachment.name) { // if the attachment we are changing is currently active, change the the live texture let region: core.TextureRegion = attachment.region if (texture) { region = new core.TextureRegion() region.texture = texture region.size = size slot.hackRegion = region slot.hackAttachment = currentAttachment } else { slot.hackRegion = null slot.hackAttachment = null } if (slot.currentSprite && slot.currentSprite.region != region) { this.setSpriteRegion(currentAttachment, slot.currentSprite, region) slot.currentSprite.region = region } else if (slot.currentMesh && slot.currentMesh.region != region) { this.setMeshRegion(currentAttachment, slot.currentMesh, region) } return true } return false }
  4. As it stands currently the issue is two parts: 1) `hackTextureBySlotIndex` will modify the *current* texture of an attachment in a slot, however if a slot has multiple attachments the attachment whose texture gets changed is whichever one is currently active. So this method gives an instant result, but won't change the hidden attachments. If called during an animation that changes attachments (esp if multiple frames), the results can be a bit random as it'll simply replace the texture in one frame of the animation. 2) using `getAttachmentByName` + `attachment.region.texture = newTexture` one can change the textures of attachments (instead of the texture of whichever current attachment is active in a slot) however this has no instant visual effect, and the new texture only appears the next time the spine entity changes to this texture So the solution for me was to mix the methods. The attachment's texture should be changed, which will do a deep reskin of the skeleton including its hidden attachments and something like hackTextureBySlot should be used to change all of the currently visible attachments. Note: when i say deep reskin i'm referring to changing all of the textures in the model with new textures using pixi, rather than any of the spine skin features which I'm not using. The second part was to make a version of hackTextureBySlotIndex that would only change the texture of the attachment if the attachment was active. const changeAttachment = (spine, slotName, attachmentName, texture) => { // changes the texture of an attachment at the skeleton level // (will not change currently visible attachments) const slotIndex = spine.skeleton.findSlotIndex(slotName) const attachment = spine.skeleton.getAttachmentByName(slotName, attachmentName) attachment.region.texture = texture // changes currently visible attachements // note: this is a modified version of hackTextureBySlotIndex changeAttachmentTextureIfActive(spine.skeleton, slotIndex, attachmentName, texture) } // modified to not change the texture of an attachment unless that attachment is currently active const changeAttachmentTextureIfActive = (skeleton, slotIndex, attachmentName, texture, size = null) => { const slot = skeleton.slots[slotIndex] if (!slot) { return false } const attachment = slot.getAttachment() if (attachmentName !== attachment.name) { // do not change the texture of this attachment return } let region = attachment.region if (texture) { region = new TextureRegion() region.texture = texture region.size = size slot.hackRegion = region slot.hackAttachment = attachment } else { slot.hackRegion = null slot.hackAttachment = null } if (slot.currentSprite && slot.currentSprite.region != region) { setSpriteRegion(attachment, slot.currentSprite, region) slot.currentSprite.region = region } else if (slot.currentMesh && slot.currentMesh.region != region) { console.log('mesh regions are disabled for changeAttachmentTextureIfActive') //this.setMeshRegion(attachment, slot.currentMesh, region) } return true } I'm going to probably combine the code from the above into another function, but I figured I'd paste it to the forum while it still resembles the existing `hackTextureBySlotIndex` closely. One could probably combine the two and call it `hackAtachmentByName(slotName, attachmentName, newTexture)` if it warranted adding to the pixi-spine api, though it only would be helpful for people who do a lot of attachment swapping in their animations. Publicly exposing TextureRegion might be helpful too for more hacks. It's also worth noting that attachments in spine are not freely nameable -- they take on the name of the images, which makes me feel like i'm sort of hacking an extra feature into spine that was unintended by doing this. (screenshot showing a spine skeleton whose attachments are named for programmatic use, as opposed to things like "iron-pick-axe.png") An animation with changing attachments (the legs use different sprites for front/back at different points in the run): In-game animations and gear changing randomly during the animation using the above code:
  5. Okay, I'll post back if I can figure out something decent.
  6. I've got a spine animation where the sprite of a certain slot changes. This is essentially the same thing as in the spine demo where they make a character blink by having two different images for the eye and having an animation that swaps between them. Currently, due to spritesheets + pixi being superior imo to how spine wants to work, I use `hackTextureBySlotName` for everything. Now my question is how I handle the situation where a slot has multiple images that are changed during an animation? For example I have code like this: model.hackTextureBySlotName('head', Texture.from(`/images/${ skin }/head.png`)) model.hackTextureBySlotName('thorax', Texture.from(`/images/${ skin }/torso.png`)) model.hackTextureBySlotName('pelvis', Texture.from(`/images/${ skin }/pelvis.png`)) // etc for all bodyparts of my skin, and gear held in the hands But I would hypothetically need something like this for the slots that had multiple images: model.hackTexturesBySlotName('left-upper-leg',[ Texture.from(`/images/${ skin }/left-upper-leg-frame-1.png`), Texture.from(`/images/${ skin }/left-upper-leg-frame-2.png`), Texture.from(`/images/${ skin }/left-upper-leg-frame-3.png`) ]) Then my spine animation which has multiple images for the leg which it switches through would work. I've attached a gif showing my animation, and how it defaults to the back of the legs turning white where the original animation would have "attached" a different piece of artwork -- the other frames of my leg art. The picture from the spine hierarchy is pointing to the two images that are switched between with red and blue. Also here's the source code for hackTextureBySlotName incase anyone could point me to a way to modify it. I was surprised that the region was a single object and not an array, but maybe I don't really understand how spine structures the skeleton. hackTextureBySlotIndex(slotIndex: number, texture: PIXI.Texture = null, size: PIXI.Rectangle = null) { let slot = this.skeleton.slots[slotIndex]; if (!slot) { return false; } let attachment: any = slot.getAttachment(); let region: core.TextureRegion = attachment.region; if (texture) { region = new core.TextureRegion(); region.texture = texture; region.size = size; slot.hackRegion = region; slot.hackAttachment = attachment; } else { slot.hackRegion = null; slot.hackAttachment = null; } if (slot.currentSprite && slot.currentSprite.region != region) { this.setSpriteRegion(attachment, slot.currentSprite, region); slot.currentSprite.region = region; } else if (slot.currentMesh && slot.currentMesh.region != region) { this.setMeshRegion(attachment, slot.currentMesh, region); } return true; } Thank you!
  7. If the tilemap isn't mutatable (or if only a few layers are mutable) one can also get away with: keeping the map data in some simple form, like an array per layer containing ints for tiletype generating megatiles by drawing sections of the map (e.g. 16x16 tiles) onto a rendertexture add the megatiles to the game instead of individual tiles (maybe) hide megatiles that are offscreen (if your player only sees a small subsection of a much larger world) Baking a large number of tiles into a megatile takes a little bit of time (milliseconds, usually) but baking an entire map depending on the size can take < 1s on a gaming rig and multiple seconds on a chromebook -- so just keep in mind that while this may help reach and maintain maximum frames per second it does so at the expense of potentially lengthy operation on slower machines as the game starts up. If you need it to generate these megatiles on the fly as the player moves around (which would not be the case for 5000 sprites, but might be the case for a super big map) then it becomes prohibitive as it makes the game choppy unless one can bake the megatile without freezing the game for a few frames. I've got a few games that work this way.. generally speaking they're able to get 60 fps on a low end chromebook... and this approach added ~4-12 seconds of 'bake time' on low end devices for a map that was ~150,000 sprites and resulted in 8096x8096 pixels worth of render texture (baked into smaller textures like 1024x1024).
  8. Nevermind! The bug was that indices are an Int32Array and I had turned them into Float32s.
  9. Thanks @bubamara and @ivan.popelyshev I've been able to mutate my mesh a bit now. I'm stuck trying to change the indices, any idea what I'm doing wrong? const vertBuffer = geometry.getBuffer('aVertexPosition') vertBuffer.update(new Float32Array(vertices)) // works const colorBuffer = geometry.getBuffer('aVertexColor') colorBuffer.update(new Float32Array(colors)) // works const indexBuffer = geometry.getIndex() indexBuffer.update(new Float32Array(indices)) // makes my mesh disappear All of this except changing the indices seems to work. If I change the indices my object vanishes. The `vertices`, `colors` and `indices` aren't actually changing -- i'm just creating the same circle over and over again and trying to update the buffers.. so maybe I have a more basic issues that is making my shape disappear. Here is the shape that I'm drawing (black lines and green points added for debugging, the mesh is the whole lighting overlay of a radial gradient within the circle and shadows in the other triangles).
  10. I'm trying to create a mesh that consists of a few dozen triangles that are going to change every frame (hopefully performs okay....) How do I actually do that though? Create mesh: const shader = Shader.from(vertexSrc, fragSrc) const geometry = new Geometry() .addAttribute('aVertexPosition', [initialVerts]) .addAttribute('aVertexColor', [initialColors]) const mesh = new Mesh(geometry, shader, null, DRAW_MODES.TRIANGLES) Attempt at changing aVertexPosition and aVertexColor each frame: mesh.geometry.addAttribute('aVertexPosition', newVertices) mesh.geometry.addAttribute('aVertexColor', newColors) Error: Cannot read property 'updateID' of undefined; originating from GeometrySystem.updateBuffers.
  11. Okay so I only ended up making two fully functioning implementations, though I did go down a few paths just to benchmark varying techniques. I have not yet done any techniques with instancing (I've only been doing webgl for a few days and need to study some more). Hopefully someone finds this useful. So a few details about the underlying map data before getting into the specific techniques. The game is networked (nengi.js) and sends chunks to the game client as the player nears them (like minecraft more or less, but just 2D). Each chunk is an array of tiles, it happens to be a 1D array with some math to turn it back into 2D, but this detail probably doesn't matter. The map as a whole is sparse collection of chunks, which also doesn't matter too much but means that the chunks can be generated one at a time -- the whole map doesn't have to be filled in and exist when the game starts. The clientside chunk graphics generator, for both techniques below, would only generate one chunk per frame and would queue them so as to avoid ever generating too many graphics in a single frame and experiencing a noticeable hitch (sounds fancier than it is). Let's say that the chunks are 8x8 tiles, and each tile is 16x16 pixels (I tested many variants). The network data for receiving a chunk then contains the chunk coordinates and 64 bytes. If not using a network or using different dimensions then this would vary, but I'm going to stick with these numbers for the examples. They were also benchmarked on two computers which I will call the chromebook (acer spin 11, Intel HD 500) and the gaming rig (ryzen 1700, 1070 ti). The first experiment uses render textures. So it receives the 64 tiles, creates 64 sprites according to the tile types, and then takes the whole thing and bakes it into a single texture. That chunk sprite is then positioned as needed (probably at x = chunkX * chunkWidthInPixels, etc for y). On the gaming rig many varieties of chunk and tile sizes and multiple layers of tiles could be baked without any hitches. The chromebook was eventually stable at 8x8 chunks with 3 layers of tiles but anything bigger than that was producing notable hitches while generating the chunk graphics. It is also worth mentioning that the above technique *minus baking the tiles* is probably what everyone makes first -- it's just rendering every single tile as a sprite without any optimization beyond what pixi does by default. On a fast computer this was actually fine as is! Where this one runs into trouble is just on the regular rendering time for the chromebook-level device... it's simply too many sprites to keep scrolling them around every frame. The second experiment was to produce a single mesh per chunk with vertices and uvs for 64 tiles. The geometry is created after receiving the network data and the tiles in the mesh are mapped to their textures in a texture atlas. I feel like, as far as webgl options go, this one was relatively simple. The performance on this was roughly 4-6x faster than the render texture scenario (which already worked on a chromebook, barely) so altogether I was happy with this option. I was reading that there would be issues with the wrapping mode and a need to create gutters in the texture atlas to remove artifacts from the edges of the tiles as they rendered, and I'm not sure if I will need to address these later. My technique for now was to make sure the pixi container's coordinates were always integers (Math.floor) and this removed artifacts (stripes mostly) that appeared at the edge of the tiles due to their texture sampling. That's all I've tried so far, but I'm pretty satisfied with both the render texture technique and the webgl technique. I'll probably stick to the mesh+webgl version as I'm trying to use webgl more in general.
  12. I think I'll try making one of each of those for the webgl practice. Thanks Ivan!
  13. I was testing a little more, and while creating a new shader every time is a bit expensive, creating new geometry from the save vertices + uvs is cheap, and then I can put the data in an attribute on that geometry instead of as a uniform. I'm new to webgl so not sure if that is the right way, but seems promising.
  14. Hello! Loving v5 it's great! I've been trying to render an extremely large map of tiles in smaller chunks that load in as the player moves around. I've done this with sprites and a render texture per chunk and it works fine, but now to learn some webgl I'm porting the project to use pixi Mesh + Shader. I've gotten to the point where I have the vertices and uv coords for a 16x16 chunk of tiles (skipping the code, nothing special). I then create a mesh per chunk of tiles and position them around. Code looks like this: const shader = PIXI.Shader.from(vertexSrc, fragmentSrc, uniforms); const chunk = new PIXI.Mesh(geometry, shader); // etc for many chunks and then I just change the chunk.x and chunk.y Now what I'm trying to do next is actually show different textures for each tile within each chunk, for which I'm using a collection of tileTypes which is either going to be an array or texture uniform with the data for the 256 tiles that comprise the 16x16 chunk. I hope that makes sense. In any case, because all of the chunks have the same geometry and the same shader if i change the `chunk.shader.unforms.tileType` it changes all of the chunks at the same time. If I create a new shader for each chunk so they have a unique uniforms object each, it ends up being an expensive operation which creates a visual hitch. I could probably create a pool of meshes and shaders and reuse them such that I wouldn't have to actually create new shader objects at run time as chunks load into the game, but before going down that path I wanted to know if there was an easier way. Can I somehow create meshes that share the geometry, vertexShader, fragmentShader, but have a *unique* uniform per instance of the mesh? Thanks
  15. GC in javascript is frequently undergoing changes, but for the last few years (if this hasn't just changed...) one of the main problems is that it isn't occurring "in the background when the program is idle" like most people assume. In truth the biggest GC hit occurs while creating a new object -- i know that sounds unbelievably bad but unless it has recently changed that's what is happening. The reason GC is invoked while creating a new object is that the trigger for GC is based off of trying to allocate new memory but discovering too much memory is already used. So right in the middle of important game code it'll periodically perform a round of garbage collection, and then finish creating that new object. This is how one can end up with relatively low CPU usage but still GC-related hitches... the GC is happening at basically the worst time every time. So that detail of GC may change in the future, but if experiencing GC problems here are the two tricks I use most frequently... As @ivan.popelyshev noted a local variable can prevent the GC. We can take a loop that used to create a new Point() (or several) as part of its internal logic. If that object is declared outside of the loop and has its values changed within the loop, then it'll only be created once in total. The second is pooling... but it is important to really understand pooling's benefit else we simply re-invent the GC lag spikes. If a pool dynamically grows and shrinks much then the GC issue will be very similar. Also pools in the past were used to speed up object creation which I'm just going to say is rarely a benefit nowadays. The best pools for mitigating GC are pools that never (or very rarely) free anything at all. For the things that actually benefit from pooling, such as projectiles in bullet-hell game, or particles, this is somewhat reasonable feature. Just create 10,000 particles (or w/e) and recycle them as needed.