As was foretold, we've added advertisements to the forums! If you have questions, or if you encounter any bugs, please visit this thread: https://forums.penny-arcade.com/discussion/240191/forum-advertisement-faq-and-reports-thread/
Options

[Game Dev] I don't have a publisher. What I do have are a very particular set of skills.

1646567697092

Posts

  • Options
    LD50LD50 Registered User regular
    That seems like a good next step. It's also worth looking at the ai script and how it's controlling itself (does it actually check player_turn; does it act properly on that check, etc)

  • Options
    Endless_SerpentsEndless_Serpents Registered User regular
    Just my annual reminder y’all video game makers are swell folks. Keep at it and have fun!

  • Options
    ElvenshaeElvenshae Registered User regular
    LD50 wrote: »
    That seems like a good next step. It's also worth looking at the ai script and how it's controlling itself (does it actually check player_turn; does it act properly on that check, etc)

    Good call.

    The enemy AI script itself doesn't deal with the player's turn; the little watcher script does. That outputs debug text each time it's invoked - at the end of the player attack functionality and the end of the player movement functionality. Each of those cases seems to work well - e.g., the player presses "m" to move, the debug text indicates that the player is trying to move, the movement UI happens, debug text gets spit out each time the player tries to move to a spot (invalid move target gives an error message; valid move target says you've picked your move spot and lists it out in the log), the "actually move" stuff happens, there's some cleanup, then the watcher script is invoked.

    The watcher script says either "It's still the player's turn" if you've moved but not attacked or attacked but not moved and then exits. If you've done both, it does the turn switch and then starts invoking enemy AI.

  • Options
    LD50LD50 Registered User regular
    Elvenshae wrote: »
    LD50 wrote: »
    That seems like a good next step. It's also worth looking at the ai script and how it's controlling itself (does it actually check player_turn; does it act properly on that check, etc)

    Good call.

    The enemy AI script itself doesn't deal with the player's turn; the little watcher script does. That outputs debug text each time it's invoked - at the end of the player attack functionality and the end of the player movement functionality. Each of those cases seems to work well - e.g., the player presses "m" to move, the debug text indicates that the player is trying to move, the movement UI happens, debug text gets spit out each time the player tries to move to a spot (invalid move target gives an error message; valid move target says you've picked your move spot and lists it out in the log), the "actually move" stuff happens, there's some cleanup, then the watcher script is invoked.

    The watcher script says either "It's still the player's turn" if you've moved but not attacked or attacked but not moved and then exits. If you've done both, it does the turn switch and then starts invoking enemy AI.

    I would take a close look at that watcher script then.

  • Options
    ElvenshaeElvenshae Registered User regular
    edited April 2020
    LD50 wrote: »
    Elvenshae wrote: »
    LD50 wrote: »
    That seems like a good next step. It's also worth looking at the ai script and how it's controlling itself (does it actually check player_turn; does it act properly on that check, etc)

    Good call.

    The enemy AI script itself doesn't deal with the player's turn; the little watcher script does. That outputs debug text each time it's invoked - at the end of the player attack functionality and the end of the player movement functionality. Each of those cases seems to work well - e.g., the player presses "m" to move, the debug text indicates that the player is trying to move, the movement UI happens, debug text gets spit out each time the player tries to move to a spot (invalid move target gives an error message; valid move target says you've picked your move spot and lists it out in the log), the "actually move" stuff happens, there's some cleanup, then the watcher script is invoked.

    The watcher script says either "It's still the player's turn" if you've moved but not attacked or attacked but not moved and then exits. If you've done both, it does the turn switch and then starts invoking enemy AI.

    I would take a close look at that watcher script then.

    AHAHAHAHAHAH!

    I commented out all of the AI stuff, and it worked flawlessly. Player moves, player attacks, watcher script says "Your turn is done," watcher script says "It's the badguys' turn," watcher script says, "Badguys are done, it's your turn now," player turn variables are reset, player can take another turn, and loop, loop, loop.

    So, that worked.

    Buuuuut ... when I turned the AI back on, I noticed that when it moved the enemy sprite, I moved them to the wrong spot in the room (row * tile_width + (tile_width / 2) instead of - (tile_width / 2), etc.). I'm not sure why that would cause everything to absolutely freak out (given the lack of collisions I have defined in the code at the moment), but it absolutely does.

    Whew!

    Elvenshae on
  • Options
    ElvenshaeElvenshae Registered User regular
    Actually, in retrospect, I do know why it was freaking out.

    1) I do have one set of collisions defined for testing purposes, and Gamemaker Studio determines collisions based on sprite location. So, even though the logical grid was correct, and the only non-debug way to move anything was the ai, and the ai will never move into a location that already has something in it, the sprite being offset +1 x, +1y means that the object would think it's moving to the correct (empty) place but then would actually end up in an impossible spot, which occasionally would be a wall or room feature, running the collision script.

    2) The player sprite wasn't disappearing; it was just having badguys offset onto it. Since the sprites aren't layered yet (and are programmer art besides), that means the player sprite would appear to disappear underneath the badguy sprite. Since the player object is always created after the badguy objects, I think the badguy sprites are always drawn first.

  • Options
    dipuc4lifedipuc4life ... In my own HeadRegistered User regular
    Bad guy sprite 'delete' player sprite ... sounds logical to me.

    NNID: SW-6490-6173-9136
    Playstation: Dipuc4Life
    Warframe_Switch IGN: ONVEBAL
  • Options
    kaceypkaceyp we stayed bright as lightning we sang loud as thunderRegistered User regular
    Okay, so I'm having an issue with gravity in Unity and figured I'd see if anyone had ideas.

    I'm making a 2D sidescrolling game. My player object uses a Rigidbody 2D and Circle Collider 2D with a physics-based movement script. I decided I wanted to add some objects that the player could trip over, falling forward to the ground.

    An idea I came across was to use root motion in order to animate position based on local position (so that I could have the forward motion of the fall built into the animation itself). So I checked root motion in the Animator and....

    Now my player character slowly floats into the air. Based on everything I've read, it seems to have something to do with the Animator applying its own gravity now? A solution I saw in a couple places was adding the float "GravityWeight" to the Animator and setting it at a constant of 1 for your animations, but that doesn't seem to work. Unless I have to add it to literally every animation in the animator, but I'm not sure why it would matter except for animations currently running.

    Unfortunately, just turning root motion back off now causes my player to just run in place if I try to move. If I disable the Animator completely, he can move around and jump just fine, but of course has no animation. I even tried Ctrl+Z undoing everything to before I made that change and it's still doing that.

    It doesn't seem to matter what properties I change on the Rigidbody. Mass doesn't change anything. Gravity only stops him from floating if I set it to 0, but then he's stuck at that Y position. The further gravity moves from 0 (positive or negative), the faster he floats away.

    Any thoughts? It's super frustrating because I had just started figuring out some gameplay stuff, and now I'm stuck trying to fix something that had been working fine.

  • Options
    ElvenshaeElvenshae Registered User regular
    kaceyp wrote: »
    Now my player character slowly floats into the air.

    Reminds me of an old joke about physicists. "Oops! Missed a sign change."

    Game physics is completely beyond me at the moment, so I don't think I provide any real help.

  • Options
    ElvenshaeElvenshae Registered User regular
    Okay - now that I've got the basics of attacks and movement working in my little turn-based game, I've technically got something that you can play and win or lose.

    The player can move around the room in a mostly-correct turn-based fashion, attack badguys (and possibly kill them), and end their turn early if they want to; the bad guys will move up next to the player when there's space available, each one nearby will attack the player (and possibly kill them).

    If the player successfully kills all the badguys, the player wins and gets a "You win!" screen (I resisted the temptation to go with "A winner is you!"). If the enemies successfully kill the player, the player loses and gets a "You have lost!" screen.

    Ergo, I have a game.

    It is now time, therefore, to rip it completely apart because creating special attacks or special moves in my current idiom is just ... uh ... painful? So now, I'm thinking of, instead of invoking a script when it's time to attack something, that attacks are actually objects with their own variables and handling, etc. This should allow me to easily keep track of who is attack whom as I float up and down the various draw / step calls.

    This may completely blow the whole thing up, but I think it's gonna work. We'll see, anyway ... :D

  • Options
    CornucopiistCornucopiist Registered User regular
    kaceyp wrote: »
    Okay, so I'm having an issue with gravity in Unity and figured I'd see if anyone had ideas.

    I'm making a 2D sidescrolling game. My player object uses a Rigidbody 2D and Circle Collider 2D with a physics-based movement script. I decided I wanted to add some objects that the player could trip over, falling forward to the ground.

    An idea I came across was to use root motion in order to animate position based on local position (so that I could have the forward motion of the fall built into the animation itself). So I checked root motion in the Animator and....

    Now my player character slowly floats into the air. Based on everything I've read, it seems to have something to do with the Animator applying its own gravity now? A solution I saw in a couple places was adding the float "GravityWeight" to the Animator and setting it at a constant of 1 for your animations, but that doesn't seem to work. Unless I have to add it to literally every animation in the animator, but I'm not sure why it would matter except for animations currently running.

    Unfortunately, just turning root motion back off now causes my player to just run in place if I try to move. If I disable the Animator completely, he can move around and jump just fine, but of course has no animation. I even tried Ctrl+Z undoing everything to before I made that change and it's still doing that.

    It doesn't seem to matter what properties I change on the Rigidbody. Mass doesn't change anything. Gravity only stops him from floating if I set it to 0, but then he's stuck at that Y position. The further gravity moves from 0 (positive or negative), the faster he floats away.

    Any thoughts? It's super frustrating because I had just started figuring out some gameplay stuff, and now I'm stuck trying to fix something that had been working fine.

    It seems to me that if you turn on the 'root motion' that would logically defer the physics to the animator, and that would mean that indeed you need to add 'GravityWeight' to every animation that needs gravity (such as interacting with a floor).

    In any case, I frequently find that things get cached and then resetting them does nothing because the editor refers to the cache. Rebuilding cache afaik used to be possible with a button but now requires you finding the cache folder and deleting it, then reimporting the project.

  • Options
    templewulftemplewulf The Team Chump USARegistered User regular
    Elvenshae wrote: »
    Okay - now that I've got the basics of attacks and movement working in my little turn-based game, I've technically got something that you can play and win or lose.

    The player can move around the room in a mostly-correct turn-based fashion, attack badguys (and possibly kill them), and end their turn early if they want to; the bad guys will move up next to the player when there's space available, each one nearby will attack the player (and possibly kill them).

    If the player successfully kills all the badguys, the player wins and gets a "You win!" screen (I resisted the temptation to go with "A winner is you!"). If the enemies successfully kill the player, the player loses and gets a "You have lost!" screen.

    Ergo, I have a game.

    It is now time, therefore, to rip it completely apart because creating special attacks or special moves in my current idiom is just ... uh ... painful? So now, I'm thinking of, instead of invoking a script when it's time to attack something, that attacks are actually objects with their own variables and handling, etc. This should allow me to easily keep track of who is attack whom as I float up and down the various draw / step calls.

    This may completely blow the whole thing up, but I think it's gonna work. We'll see, anyway ... :D

    I forget, are you working in Unity? Their Scriptable Objects construct is incredibly helpful for making objects that hold data like that.

    Twitch.tv/FiercePunchStudios | PSN | Steam | Discord | SFV CFN: templewulf
  • Options
    kaceypkaceyp we stayed bright as lightning we sang loud as thunderRegistered User regular
    I think I fixed it. Since the animator was apparently causing the issue, I just...replaced it. I created a new animator controller, and copied the two most necessary animations (idle and running) into it to test.

    Everything seems to be working fine with the new animator. No more floating OR running in place.

    So by fixed I of course mean that I couldn't figure out the real problem, so I just threw the thing in the trash where it can't hurt me and got a new one.

  • Options
    ElvenshaeElvenshae Registered User regular
    templewulf wrote: »
    Elvenshae wrote: »
    Okay - now that I've got the basics of attacks and movement working in my little turn-based game, I've technically got something that you can play and win or lose.

    The player can move around the room in a mostly-correct turn-based fashion, attack badguys (and possibly kill them), and end their turn early if they want to; the bad guys will move up next to the player when there's space available, each one nearby will attack the player (and possibly kill them).

    If the player successfully kills all the badguys, the player wins and gets a "You win!" screen (I resisted the temptation to go with "A winner is you!"). If the enemies successfully kill the player, the player loses and gets a "You have lost!" screen.

    Ergo, I have a game.

    It is now time, therefore, to rip it completely apart because creating special attacks or special moves in my current idiom is just ... uh ... painful? So now, I'm thinking of, instead of invoking a script when it's time to attack something, that attacks are actually objects with their own variables and handling, etc. This should allow me to easily keep track of who is attack whom as I float up and down the various draw / step calls.

    This may completely blow the whole thing up, but I think it's gonna work. We'll see, anyway ... :D

    I forget, are you working in Unity? Their Scriptable Objects construct is incredibly helpful for making objects that hold data like that.

    Gamemaker Studio 2.

    I had to rip up a whole bunch of my initial monster spawning and game grid code because chained accessors were supposed to be in last year some time, and the beta with them just recently went into limited release.

    I found that out after the fact, but it was a good intro to their forums. :D

  • Options
    KupiKupi Registered User regular
    Kupi's Weekly Friday Game Dev Status Report

    In order to avoid having nothing to report this week (the design work for control management is not going well), I quickly grabbed one of my todos off the shelf and handled it. My "override / load from local storage" content manager can now load sound effects. Therefore, here's Sonic going BWEEP as he jumps.

    https://www.youtube.com/watch?v=yxY-j3JZofo

    My favorite musical instrument is the air-raid siren.
  • Options
    templewulftemplewulf The Team Chump USARegistered User regular
    I'm trying out Godot for the first time, and... I think I love it????

    The signals, especially, really call to me as an OOP diehard. https://docs.godotengine.org/en/stable/getting_started/step_by_step/signals.html

    I think I might try to make a basic 1P crafting / survival proof of concept this weekend in it.

    Twitch.tv/FiercePunchStudios | PSN | Steam | Discord | SFV CFN: templewulf
  • Options
    GlalGlal AiredaleRegistered User regular
    My experience with Godot has been very positive, it's just the right amount of high level ease and low level control. Plus, I get to use (real) Python, which is lovely.

  • Options
    templewulftemplewulf The Team Chump USARegistered User regular
    Glal wrote: »
    My experience with Godot has been very positive, it's just the right amount of high level ease and low level control. Plus, I get to use (real) Python, which is lovely.
    So far, I very agree!

    Is GDScript full python or just a subset?

    Twitch.tv/FiercePunchStudios | PSN | Steam | Discord | SFV CFN: templewulf
  • Options
    GlalGlal AiredaleRegistered User regular
    Its own thing, but it uses Python syntax, which for me personally is the important bit, I can always learn new libraries, I do that all the time anyway.

  • Options
    AkimboEGAkimboEG Mr. Fancypants Wears very fine pants indeedRegistered User regular
    The important differences (for gamedev) are that GDScript is a compiled language, and that it has built-in support for vectors (which python doesn't, except through third party libraries).

    I really like it! Godot is great.

    Give me a kiss to build a dream on; And my imagination will thrive upon that kiss; Sweetheart, I ask no more than this; A kiss to build a dream on
  • Options
    ElvenshaeElvenshae Registered User regular
    edited May 2020
    Elvenshae wrote: »

    This may completely blow the whole thing up, but I think it's gonna work. We'll see, anyway ... :D

    Narrator: It didn't work. It was close, though.

    But! I found a tutorial on YouTube that went through making a GMS turn-based strategy game based loosely on D&D5E, and I'm most of the way through it. It's overall been a complete improvement in the way I did things before, and it's close enough to what I want to create that it aligns mostly with my work and I should be able to extend it into what I want to do.

    He did his in GM1, though, and I'm in GM2, so I've had to make some changes - but I've mostly been able to adapt them.

    EXCEPT.

    There's a small difference in the way we did things waaaaaay back in the first video. He used a globalvar for his map (a 2d array of node IDs), and I used a data-structure grid of node IDs tied to the main game object (which is persistent). It works completely, totally the same (minus some terminology differences) up until I need to reset the game room when the player loses.

    When that happens, I get some absolutely weirdshit errors.

    So, I think I'm going to need to do some surgery on my main game grid, promoting it to a global variable data structure, and see what breaks.

    Elvenshae on
  • Options
    ElvenshaeElvenshae Registered User regular
    edited May 2020
    Motherf.

    I promoted the variable to a global, and that seems to address one issue I had, but now ...

    This is a character marking which squares they can move to on their turn (teal = 1 move action; yellow = 2 move actions). It does this by setting the color variable of the node object, which is illustrated as a blank 5' square except where there was terrain "above" it at the start of the game, where it takes on the sprite ID of the terrain (and a couple of other pertinent data points, like "is a wall" or "is difficult terrain"). The "goblin target" is red because the Fighter can move to it and attack this turn.

    rrchaaxccjni.png

    This is the same Fighter after I've debug-killed all the PCs, thus triggering a room restart:

    qqsqcal1h434.png

    The game has forgotten how to recolor nodes that aren't difficult terrain: all the goblins and all the empty squares should be filled in with teal, yellow, or red.

    Ugh.

    ED: And, after the reset, if I walk into difficult terrain and end my turn there, I cannot get out again. Something definitely fucky is going on here.

    Elvenshae on
  • Options
    ElvenshaeElvenshae Registered User regular
    edited May 2020
    ... So, adding a "destroy all instances" command to the main game controller in the "end room" timing fixes the room transitions. Need to see if just blowing up the nodes (and creatures?) is enough. I've got a pathfinding bug, though, in my second room:

    bu26fasi0a6n.png

    The one in the middle should be a walkable square.

    EDIT: Destroying the nodes works. Whew!

    Elvenshae on
  • Options
    ElvenshaeElvenshae Registered User regular
    Way too proud of this, but I found out where the fuckery was happening. For some reason, nodes were dropping the neighbor information that's key to pathfinding when they took on the information from the terrain above them. Neighbor count debug text for the win!

    0jg193vfl9fz.png

    So the difficult terrain is a neighbor to the non-difficult terrain surrounding it, but it itself has no neighbors. So, it was not properly counting pathfinding through the squares, and then once you landed in one you don't have any neighbors you can walk to, so you're stuck forever.

    Turns out, when I set up inheritance, that resulted in the node creation script running again when it took over the terrain data, resulting in them resetting the neighbors list. It works now!

    6da4h6m4tc73.png

  • Options
    KupiKupi Registered User regular
    Kupi's Weekly Friday Status Report

    Under the increasingly-familiar pall of inspirationlessness, I decided to give Sonic the ability to align himself to the surface normal of platforms as an incremental step to making something that looks more like Sonic gameplay. The token visual demonstration of this behavior follows:

    https://youtu.be/U0OrHMy0Jqk

    Observe that I've given him zero ability to re-align himself in midair, which is mildly hilarious.

    Then, somewhere around Tuesday, I realized that I had recently extracted my THIRTY GIGS OF TOUHOU FAN-MUSIC from the external drive that I'm using as a back-up medium, plugged the entire damn thing into VLC, and spent a solid four hours in a haze of techno and keyboard clacks. I worked on a data structure to replace the linked-list that I use to track unresolved collision events in the collision detection system, because anything that requires you to do a linear scan through it every single time you invalidate a collision has to be your performance bottleneck, right? I had an immense amount of fun building what is, for all intents and purposes, a linked list and a binary tree sharing a trenchcoat, and very nearly had the entire thing unit-tested and ready to put into production before I decided to go back and look at the CPU profile of my worst-case stress test, and discovered that the "merge new collision events in" function was on the stack for a whole 0.2% of my total CPU time. That's not 0.2 as in a twentieth, that's 0.2 percent. So I decided, you know what, maybe this wasn't the best use of my time.

    So actually paying attention to the CPU profiler this time, I noticed that my object pool's allocation method was showing something like 14% total time on the stack. Which makes a certain amount of sense since I've fastidiously pooled every reference type I've written so far. Looking at the code, the problem is immediately obvious. Here's a rough sketch of how object allocation works:
    object ObjectPool.Allocate(Type type)
    {
        // lock the map of types to pools
            // get the type's pool from the map of types to pools, or create it if it doesn't exist
        // lock the type's pool
            // get the next available instance from the pool
    }
    

    The map from types to object pools and the object pools themselves are locked because this is a static method and it might be accessed from any thread. Collections don't like it when you modify them from two threads at once. However, the more locks you have, the more contention you have between threads, which means waiting, which means wasted CPU. And considering that the whole point of pooling objects is to avoid wasting CPU by way of not provoking the garbage collector, well, that seems counter-productive.

    So, for the next two days, I set out entirely reworking my approach to pooled objects. Whereas previously the object pool was a static class, now I have a PooledObject type that serves as a new base type for anything that wants to be pooled. It looks something like this (the actual version is more complicated; I've elided some extra bits):
    public abstract class PooledObject<T> where T : PooledObject<T>
    {
        static Stack<T> pool = new Stack<T>();
        
        public static T Allocate()
        {
            lock(pool)
            {
                if(pool.Count > 0)
                {
                    return pool.Pop();
                }
                else
                {
                    return (T)Activator.CreateInstance(typeof(T));
                }
            }
        }
        
        public void Release()
        {
            lock(pool)
            {
                pool.Push(this);
            }
        }
    }
    

    The interesting bit is that this is a generic type whose type parameter is constrained to be an instance of the base type parameterized with itself. This is similar to the Curiously Recurring Template Pattern in C++, so called because when it was first discovered, the programmers who discovered it were confident it wouldn't compile and baffled when it did. It has the useful property of allowing the base type to reference the derived type. In practical terms, let's say that we want to have two different types of pooled objects, Dog and Cat. Dog is declared as "class Dog : PooledObject<Dog>" and Cat is declared as "class Cat : PooledObject<Cat>". The return type from Dog.Allocate() is Dog, and the return type from Cat.Allocate() is Cat. Using either allocates instances from a matching pool. The end result is that we bypass the first layer of locking that the old object pool implementation was subject to, and some other logic I had implemented because much simpler, computationally-speaking. The end result was that my stress-test's performance improved............ slightly.

    Then, today, literally like an hour before I set out to write this post, I realized something. I update every collision volume's position in the broadphase grid every update. That operation is also subject to all kinds of lock contention (in a similar structure to the "lock the big dictionary, then lock the small dictionary you get out of it" that the old object pooling system used). However, half the objects in my stress-test, and realistically speaking 90% of all objects ever, don't actually move and therefore won't change their grid position once assigned. I added a new check, one line of code, to make sure that if the object isn't actually moving, I don't update its broadphase grid coordinates. It didn't solve everything, it didn't give me hundreds of thousands of objects in play at once, but it took more time off the average frame time than everything else I'd done this week put together.

    §(* ̄▽ ̄*)§ POGRAMMIMG

    My favorite musical instrument is the air-raid siren.
  • Options
    IzzimachIzzimach Fighter/Mage/Chef Registered User regular
    I was not really in a state to do programming this week so I doodled/fiddled with some art assets.

    Here is a pile of dirt that represents the entrance to a burrow/tunnel.

    489ILwi.jpg

    And here's a cable that you can walk along like the ziplines in Warframe. Gotta re-map the texture and make a better shader for it.

    iTEMkjg.jpg


  • Options
    PhyphorPhyphor Building Planet Busters Tasting FruitRegistered User regular
    Kupi wrote: »
    Kupi's Weekly Friday Status Report

    Under the increasingly-familiar pall of inspirationlessness, I decided to give Sonic the ability to align himself to the surface normal of platforms as an incremental step to making something that looks more like Sonic gameplay. The token visual demonstration of this behavior follows:

    https://youtu.be/U0OrHMy0Jqk

    Observe that I've given him zero ability to re-align himself in midair, which is mildly hilarious.

    Then, somewhere around Tuesday, I realized that I had recently extracted my THIRTY GIGS OF TOUHOU FAN-MUSIC from the external drive that I'm using as a back-up medium, plugged the entire damn thing into VLC, and spent a solid four hours in a haze of techno and keyboard clacks. I worked on a data structure to replace the linked-list that I use to track unresolved collision events in the collision detection system, because anything that requires you to do a linear scan through it every single time you invalidate a collision has to be your performance bottleneck, right? I had an immense amount of fun building what is, for all intents and purposes, a linked list and a binary tree sharing a trenchcoat, and very nearly had the entire thing unit-tested and ready to put into production before I decided to go back and look at the CPU profile of my worst-case stress test, and discovered that the "merge new collision events in" function was on the stack for a whole 0.2% of my total CPU time. That's not 0.2 as in a twentieth, that's 0.2 percent. So I decided, you know what, maybe this wasn't the best use of my time.

    So actually paying attention to the CPU profiler this time, I noticed that my object pool's allocation method was showing something like 14% total time on the stack. Which makes a certain amount of sense since I've fastidiously pooled every reference type I've written so far. Looking at the code, the problem is immediately obvious. Here's a rough sketch of how object allocation works:
    object ObjectPool.Allocate(Type type)
    {
        // lock the map of types to pools
            // get the type's pool from the map of types to pools, or create it if it doesn't exist
        // lock the type's pool
            // get the next available instance from the pool
    }
    

    The map from types to object pools and the object pools themselves are locked because this is a static method and it might be accessed from any thread. Collections don't like it when you modify them from two threads at once. However, the more locks you have, the more contention you have between threads, which means waiting, which means wasted CPU. And considering that the whole point of pooling objects is to avoid wasting CPU by way of not provoking the garbage collector, well, that seems counter-productive.

    So, for the next two days, I set out entirely reworking my approach to pooled objects. Whereas previously the object pool was a static class, now I have a PooledObject type that serves as a new base type for anything that wants to be pooled. It looks something like this (the actual version is more complicated; I've elided some extra bits):
    public abstract class PooledObject<T> where T : PooledObject<T>
    {
        static Stack<T> pool = new Stack<T>();
        
        public static T Allocate()
        {
            lock(pool)
            {
                if(pool.Count > 0)
                {
                    return pool.Pop();
                }
                else
                {
                    return (T)Activator.CreateInstance(typeof(T));
                }
            }
        }
        
        public void Release()
        {
            lock(pool)
            {
                pool.Push(this);
            }
        }
    }
    

    The interesting bit is that this is a generic type whose type parameter is constrained to be an instance of the base type parameterized with itself. This is similar to the Curiously Recurring Template Pattern in C++, so called because when it was first discovered, the programmers who discovered it were confident it wouldn't compile and baffled when it did. It has the useful property of allowing the base type to reference the derived type. In practical terms, let's say that we want to have two different types of pooled objects, Dog and Cat. Dog is declared as "class Dog : PooledObject<Dog>" and Cat is declared as "class Cat : PooledObject<Cat>". The return type from Dog.Allocate() is Dog, and the return type from Cat.Allocate() is Cat. Using either allocates instances from a matching pool. The end result is that we bypass the first layer of locking that the old object pool implementation was subject to, and some other logic I had implemented because much simpler, computationally-speaking. The end result was that my stress-test's performance improved............ slightly.

    Then, today, literally like an hour before I set out to write this post, I realized something. I update every collision volume's position in the broadphase grid every update. That operation is also subject to all kinds of lock contention (in a similar structure to the "lock the big dictionary, then lock the small dictionary you get out of it" that the old object pooling system used). However, half the objects in my stress-test, and realistically speaking 90% of all objects ever, don't actually move and therefore won't change their grid position once assigned. I added a new check, one line of code, to make sure that if the object isn't actually moving, I don't update its broadphase grid coordinates. It didn't solve everything, it didn't give me hundreds of thousands of objects in play at once, but it took more time off the average frame time than everything else I'd done this week put together.

    §(* ̄▽ ̄*)§ POGRAMMIMG

    ConcurrentStack should give you a lockfree container. Internally it is implemented with Interlocked* primitives. Generally you want to be optimistic with these, dont check count first, just check if pop fails

    If you want to get fancy you can do a pool per-type and per-thread to guarantee zero contention

  • Options
    KupiKupi Registered User regular
    Phyphor wrote: »
    Kupi wrote: »
    WORDS WORDS WORDS

    ConcurrentStack should give you a lockfree container. Internally it is implemented with Interlocked* primitives. Generally you want to be optimistic with these, dont check count first, just check if pop fails

    If you want to get fancy you can do a pool per-type and per-thread to guarantee zero contention

    It's interesting; I considered the Concurrent containers, but I found that in my test bed, ConcurrentStack and ConcurrentBag* both underperformed a simple Stack<T> under a lock statement by about a tenth of a millisecond's worth of average frame time. Not a huge tax, but it's detectable and consistent. My guess, based on a quick glance over at source.dot.net**, is that the logic they use to achieve the lock-free-ness is sufficiently complex compared to "has any? give, else make new" to make up the difference.

    * ConcurrentBag has the contract that best matches my use-case, where you add from anywhere and don't care which item you get out of the bag when you ask. That has the kind of per-thread behavior you mention, where every thread gets its own pile of items and only goes cross-thread when a request for more comes in on a thread that's run out, at which point it steals another thread's pile or some fraction thereof.

    ** source.dot.net is a great resource for learning about how all the guts of the .NET framework work.

    My favorite musical instrument is the air-raid siren.
  • Options
    PhyphorPhyphor Building Planet Busters Tasting FruitRegistered User regular
    Kupi wrote: »
    Phyphor wrote: »
    Kupi wrote: »
    WORDS WORDS WORDS

    ConcurrentStack should give you a lockfree container. Internally it is implemented with Interlocked* primitives. Generally you want to be optimistic with these, dont check count first, just check if pop fails

    If you want to get fancy you can do a pool per-type and per-thread to guarantee zero contention

    It's interesting; I considered the Concurrent containers, but I found that in my test bed, ConcurrentStack and ConcurrentBag* both underperformed a simple Stack<T> under a lock statement by about a tenth of a millisecond's worth of average frame time. Not a huge tax, but it's detectable and consistent. My guess, based on a quick glance over at source.dot.net**, is that the logic they use to achieve the lock-free-ness is sufficiently complex compared to "has any? give, else make new" to make up the difference.

    * ConcurrentBag has the contract that best matches my use-case, where you add from anywhere and don't care which item you get out of the bag when you ask. That has the kind of per-thread behavior you mention, where every thread gets its own pile of items and only goes cross-thread when a request for more comes in on a thread that's run out, at which point it steals another thread's pile or some fraction thereof.

    ** source.dot.net is a great resource for learning about how all the guts of the .NET framework work.

    I looked at https://referencesource.microsoft.com/#mscorlib/system/Collections/Concurrent/ConcurrentStack.cs,f0d50ad38c577f91

    Interestingly I think I spotted their mistake. If there is contention they fall back into TryPopCore, which constructs a new Random but that does a lot (https://referencesource.microsoft.com/#mscorlib/system/random.cs,92e3cf6e56571d5a) and they do it each time there is contention on pop

    ConcurrentBag is "optimized for scenarios where the same thread will be both producing and consuming data stored in the bag" which is not what you want either

  • Options
    KupiKupi Registered User regular
    Phyphor wrote: »
    Kupi wrote: »
    Phyphor wrote: »
    Kupi wrote: »
    WORDS WORDS WORDS

    ConcurrentStack should give you a lockfree container. Internally it is implemented with Interlocked* primitives. Generally you want to be optimistic with these, dont check count first, just check if pop fails

    If you want to get fancy you can do a pool per-type and per-thread to guarantee zero contention

    It's interesting; I considered the Concurrent containers, but I found that in my test bed, ConcurrentStack and ConcurrentBag* both underperformed a simple Stack<T> under a lock statement by about a tenth of a millisecond's worth of average frame time. Not a huge tax, but it's detectable and consistent. My guess, based on a quick glance over at source.dot.net**, is that the logic they use to achieve the lock-free-ness is sufficiently complex compared to "has any? give, else make new" to make up the difference.

    * ConcurrentBag has the contract that best matches my use-case, where you add from anywhere and don't care which item you get out of the bag when you ask. That has the kind of per-thread behavior you mention, where every thread gets its own pile of items and only goes cross-thread when a request for more comes in on a thread that's run out, at which point it steals another thread's pile or some fraction thereof.

    ** source.dot.net is a great resource for learning about how all the guts of the .NET framework work.

    I looked at https://referencesource.microsoft.com/#mscorlib/system/Collections/Concurrent/ConcurrentStack.cs,f0d50ad38c577f91

    Interestingly I think I spotted their mistake. If there is contention they fall back into TryPopCore, which constructs a new Random but that does a lot (https://referencesource.microsoft.com/#mscorlib/system/random.cs,92e3cf6e56571d5a) and they do it each time there is contention on pop

    ConcurrentBag is "optimized for scenarios where the same thread will be both producing and consuming data stored in the bag" which is not what you want either

    ... and, since Random is a class, allocating one creates an instance that the garbage collector will eventually kick in to clean up, which is exactly what this harebrained system is attempting to avoid. :lol: That explains why I saw so many GCs in my high-pressure tests; every contention turned into an allocation.

    My favorite musical instrument is the air-raid siren.
  • Options
    PhyphorPhyphor Building Planet Busters Tasting FruitRegistered User regular
    edited May 2020
    I'm C++ not C# here, is there a way to do stack allocs that they're missing?

    Their problem is they want to do random exponential backoff to prevent a worst-case scenario and they can't use a member because it wouldn't be threadsafe. Of course since they're just generating 1-8 it's not like they really need a full rng anyway and performance critical code like this should just take the hit and inline all their logic

    But yeah while the algorithm they are using is sane their implementation is flawed, those APIs will give you maximum* performance if used correctly though

    *in extremely high contention cases it's possible to generate so much useless cache work that it effectively slows the system down, however the equivalent locked system would just be a lock convoy which is even worse, to demonstrate this:

    Here's a microbenchmark to hammer a criticalsection+simple stack vs interlockedslist which is generally the fastest you can do in native
    #include <thread>
    #include <atomic>
    #include <vector>
    #include <random>
    
    #include <stdio.h>
    #include <Windows.h>
    
    std::atomic<bool> quit;
    
    struct E
    {
    	SLIST_ENTRY e;
    };
    struct AtomicStack
    {
    	SLIST_HEADER h;
    
    	AtomicStack()
    	{
    		InitializeSListHead(&h);
    	}
    	E* Pop()
    	{
    		return (E*)InterlockedPopEntrySList(&h);
    	}
    	void Push(E *p)
    	{
    		InterlockedPushEntrySList(&h, &p->e);
    	}
    };
    
    struct LockedStack
    {
    	CRITICAL_SECTION cs;
    	E *head = nullptr;
    
    	LockedStack()
    	{
    		InitializeCriticalSection(&cs);
    	}
    	~LockedStack()
    	{
    		DeleteCriticalSection(&cs);
    	}
    	E* Pop()
    	{
    		E *ret;
    		EnterCriticalSection(&cs);
    		ret = head;
    		if(head)
    			head = (E*)head->e.Next;
    		LeaveCriticalSection(&cs);
    		return ret;
    	}
    	void Push(E *p)
    	{
    		EnterCriticalSection(&cs);
    		p->e.Next = &head->e;
    		head = p;
    		LeaveCriticalSection(&cs);
    	}
    };
    
    template<typename T>
    void threadproc(T *stack, HANDLE startev, std::atomic<uint64_t> *count)
    {
    	std::mt19937 rng;
    	uint64_t localcount = 0;
    	WaitForSingleObject(startev, INFINITE);
    	while(!quit.load(std::memory_order_acquire)) {
    		for(int i = 0; i < 10; i++) {
    			E *e = stack->Pop();
    			if(!e)
    				e = new E();
    			else
    				localcount++;
    			stack->Push(e);
    			localcount++;
    		}
    	}
    	count->fetch_add(localcount);
    }
    
    template<typename T>
    uint64_t Test(int nthreads, uint32_t delay)
    {
    	T stack;
    	for(int i = 0; i < 1000; i++)
    		stack.Push(new E());
    	quit = false;
    	HANDLE start = CreateEvent(NULL, TRUE, FALSE, NULL);
    	std::vector<std::thread> threads;
    	std::atomic<uint64_t> counter = 0;
    	for(int i = 0; i < nthreads; i++)
    		threads.emplace_back(threadproc<T>, &stack, start, &counter);
    	Sleep(10); // Let the threads initialize
    	SetEvent(start);
    	Sleep(delay);
    	quit.store(true);
    	CloseHandle(start);
    	for(auto& t : threads)
    		t.join();
    	return counter;
    }
    
    void TestAll(int nthreads, uint32_t delay)
    {
    	uint64_t natomic = Test<AtomicStack>(nthreads, delay);
    	uint64_t ncs = Test<LockedStack>(nthreads, delay);
    
    	printf("%d threads\n", nthreads);
    	printf("Atomic: %d ops/ms\n", natomic / delay);
    	printf("CriticalSection: %d ops/ms\n", ncs / delay);
    }
    
    int main()
    {
    	printf("bits = %d\n", 8 * sizeof(void*));
    	SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_TIME_CRITICAL);
    	for(int i = 0; i < 4; i++)
    		TestAll(1<<i, 10000);
    	return 0;
    }
    

    Output
    bits = 32
    1 threads
    Atomic: 59962 ops/ms
    CriticalSection: 64765 ops/ms
    2 threads
    Atomic: 35838 ops/ms
    CriticalSection: 13334 ops/ms
    4 threads
    Atomic: 16057 ops/ms
    CriticalSection: 5112 ops/ms
    8 threads
    Atomic: 7666 ops/ms
    CriticalSection: 1943 ops/ms
    16 threads
    Atomic: 4810 ops/ms
    CriticalSection: 1029 ops/ms
    
    bits = 64
    1 threads
    Atomic: 134368 ops/ms
    CriticalSection: 65390 ops/ms
    2 threads
    Atomic: 34184 ops/ms
    CriticalSection: 13472 ops/ms
    4 threads
    Atomic: 13719 ops/ms
    CriticalSection: 6148 ops/ms
    8 threads
    Atomic: 7141 ops/ms
    CriticalSection: 2021 ops/ms
    16 threads
    Atomic: 4913 ops/ms
    CriticalSection: 1087 ops/ms
    

    The atomics will always beat locks for simple things like just having a stack, of course it is also much easier to work with locked code so /shrug. Their backoff code is trying to avoid the worst of this contention

    Phyphor on
  • Options
    CornucopiistCornucopiist Registered User regular
    Reading you guys' posts I'm really thinking I should eke out the time to take a introductory course to comp-sci. I'll likely save more time over the coming years than it would cost me. It's just that I don't have steady amounts of free time right now, only patches here and there, and that mostly multitasking on the side of work.
    I could watch youtube while cooking, though. Does anyone know of any video tutorials?

  • Options
    Alistair HuttonAlistair Hutton Dr EdinburghRegistered User regular
    Pico 8 has finally stabalised its latest release.

    It is lit. New bit twiddling operations and the tline function allow for some crazy impressive stuff whilst still being dirt simple.

    Wish I had the time to really get back into it.

    I have a thoughtful and infrequently updated blog about games http://whatithinkaboutwhenithinkaboutgames.wordpress.com/

    I made a game, it has penguins in it. It's pay what you like on Gumroad.

    Currently Ebaying Nothing at all but I might do in the future.
  • Options
    nervenerve Registered User regular
    Today I tried making a shader to fade out the model alpha/transparency when I harvested some grass in my game. I trigger a parameter change when I cut the grass in my game and it seems like everything using that material fades out at the same time. I was also thinking of using a dissolve shader to hide trees that block the camera but now it seems like every tree in the camera view would be dissolved. Does anyone know if there a way to instantiate materials so that only the grass I cut down will fade out? If not, what is the typical use of this type of shader? Would these types of shaders usually only be used in cases where a single object uses such an effect?

  • Options
    GnomeTankGnomeTank What the what? Portland, OregonRegistered User regular
    nerve wrote: »
    Today I tried making a shader to fade out the model alpha/transparency when I harvested some grass in my game. I trigger a parameter change when I cut the grass in my game and it seems like everything using that material fades out at the same time. I was also thinking of using a dissolve shader to hide trees that block the camera but now it seems like every tree in the camera view would be dissolved. Does anyone know if there a way to instantiate materials so that only the grass I cut down will fade out? If not, what is the typical use of this type of shader? Would these types of shaders usually only be used in cases where a single object uses such an effect?

    Materials and shaders are a pipeline level object in most engines. This is mostly for performance reasons. So there isn't a concept of "this instance of this material", it's just "this material".

    The way I work around this, generally, is to either swap the materials (which most engines support), or to use a proxy object. Generally as a prefab with the "alive" version and the "dead" version as a prefab with a visibility swap as the trigger action.

    That is if I am understanding what you're trying to do correctly.

    Sagroth wrote: »
    Oh c'mon FyreWulff, no one's gonna pay to visit Uranus.
    Steam: Brainling, XBL / PSN: GnomeTank, NintendoID: Brainling, FF14: Zillius Rosh SFV: Brainling
  • Options
    PhyphorPhyphor Building Planet Busters Tasting FruitRegistered User regular
    A shader/material instance is just changing the currently bound uniforms / parameters / whatever your API calls them. You still have to handle the case of fading out a new one once an already existing one is fading out so each object would probably get its own instance

    However with transparency in particular you definitely do not want to make the engine think objects can be transparent during normal use so swap to a separate material to fade out

  • Options
    nervenerve Registered User regular
    Thank you for the responses.

    I am using Unity and was replacing the original model with a "dead" model that is fractured. The issue I was running into was when I "kill" multiple grass objects in a short period of time the fade parameter resets on all objects at once. For example, if I had a one-second fade out time and kill a second grass object 0.5 seconds after the first one is killed then both object parameters would be reset to the starting point even though the first object would have been halfway through the "fade" operation - if that makes sense.

  • Options
    PhyphorPhyphor Building Planet Busters Tasting FruitRegistered User regular
    Maybe take a look at https://answers.unity.com/questions/36011/different-shader-parameters-for-instances-of-a-mat.html which seems to be the same issue you are seeing

  • Options
    DisruptedCapitalistDisruptedCapitalist I swear! Registered User regular
    edited May 2020
    Another thought: instead of trying to fade the material, perhaps have the mesh drop below the ground as a simple translation? It might even have a more realistic appearance of "cutting" the grass if you add a little rotation to it.

    Edit: actually the explanation on Phyphor's link looks like it should solve your problem too.

    DisruptedCapitalist on
    "Simple, real stupidity beats artificial intelligence every time." -Mustrum Ridcully in Terry Pratchett's Hogfather p. 142 (HarperPrism 1996)
  • Options
    nervenerve Registered User regular
    Phyphor wrote: »
    Maybe take a look at...

    Oh, wow! That sounds like exactly what I was looking for. Thank you

Sign In or Register to comment.