• 0

    posted a message on Spawning Random Waves Based on Budget + Wave Evolutions

    I'm working on a defense-type map where waves of Zerg will continually spawn and attack. Each wave has a score value based on the strength of the composite units and the frequency with which the wave will spawn and the game is given a budget with which to pick a wave to start spawning. Once a wave starts spawning, it will continue to spawn til the end.

    Periodically, the game will get an increased budget and add a new wave to the mix. The same wave can be picked more than once, up to a max number of times, but will be less and less likely to be picked again. Also the game will favor picking stronger waves over weaker ones in order to max out its budget and maintain an overall level of challenge while still being unpredictable.

    When units spawn, the game picks a random point in a given region. I believe random points may cause memory leaks so I set it to pick up to a number of random points and reuse points.

    In addition, there are wave evolutions. When a wave evolves into another wave, all existing instances of the wave are replaced with the wave. Any wave can have any number of evolutionary waves and once a wave is evolved the evolution is permanent for the rest of the game. For instance, if the game were setup to have Zerglings be able to evolve into Raptors or Swarmlings, then at the start, neither Raptors nor Swarmlings would be eligible for spawning. They would become eligible once at least 1 wave of Zerglings has started spawning. If the game then had 2 waves of Zerglings spawning and chose to start spawning Raptors, both Zergling waves would be replaced with Raptor waves and neither Zerglings nor Swarmlings would be able to spawn for the rest of the game.

    To further mix things up, while a wave has a set frequency, the actual frequency can be adjusted by up to 10% in either direction, and the wave can be delayed by up to 10 seconds to further keep things random. Otherwise you'd also have a wave starting at the same time and if there were multiples of a wave they'd always end up spawning the same amount of time apart from each other.. this mixes it up enough so that they might spawn together or far apart or anywhere in-between and it'll change each time.

    I've tested the system and I'm satisfied with the randomness and how everything works. I'd like to clean up the code and add better comments, but I need to get back to my day job and I was too anxious to get this posted.

    I'm primarily posting because I'm interested in any technical feedback. This is my first time writing something in galaxy script and I'm not sure that it's really as good as it could be. For instance, I'm using a timer for each instance of each wave. Would potentially having that many timers ever be an issue (imagine if there might be up to 100 waves)? I'm also unsure of how to "clean up" timers. I didn't see a TimerDestroy method and I'm unsure if setting them to null might cause a memory leak. Also I created a separate trigger function for each spawn, which seems redundant, but it also seemed the best way to potentially support more varied waves, like 5 zerglings + 1 queen as a wave.

    I'd also just simply like to share this with the community. I feel the system works well and can be tweaked to suit many games that have periodically spawning units. I may write it as a library and/or tutorial if there's interest in it.

    Thanks for any feedback, positive or negative! I hope someone can find use of the system.

    region BASE_REGION = RegionFromId(3);
    point BASE_POINT = RegionGetCenter(BASE_REGION);
    const int MAX_SPAWN_POINTS = 20;
    const int MAX_SPAWN_WAVE_DUPES = 5;
    const int SPAWN_OWNER = 13;
    region SPAWN_REGION_AIR = RegionFromId(4);
    region SPAWN_REGION_GROUND = RegionFromId(2);
    int waveCount = 0;
    
    void debug(string message) {
        UIDisplayMessage(PlayerGroupAll(), c_messageAreaChat, StringToText(message));
    }
    
    struct spawnWave {
        fixed frequency;
        string spawnTriggerName;
        int value; // calculated value is sum of units spawning multipled by spawns per minute
        int baseWaveIndex;
        int[2] evolvedWaveIndex;
        int evolvedWaveCount;
        int count; // same wave can spawn up to 5 times, store random delay between spawns
        timer[MAX_SPAWN_WAVE_DUPES] waveTimer;
        trigger spawnTrigger;
        int chance;
    };
    
    spawnWave[20] spawnWaves;
    int spawnWaveCount = 0;
    int spawnWaveBudget = 600;
    point[MAX_SPAWN_POINTS] spawnPointsAir;
    point[MAX_SPAWN_POINTS] spawnPointsGround;
    
    int CreateSpawnWave(fixed frequency, string spawnTriggerName, int value) {
        int index = spawnWaveCount;
        spawnWaveCount = spawnWaveCount + 1;
        spawnWaves[index].frequency = frequency;
        spawnWaves[index].spawnTriggerName = spawnTriggerName;
        spawnWaves[index].value = value;
        spawnWaves[index].baseWaveIndex = -1;
        spawnWaves[index].evolvedWaveCount = 0;
        spawnWaves[index].count = 0;
        debug("Added "+spawnTriggerName+" at index "+IntToString(index)+", count: "+IntToString(spawnWaveCount));
        return index;
    }
    
    void AddSpawnWaveEvolution(int baseWaveIndex, int evolutionWaveIndex) {
        int count = spawnWaves[baseWaveIndex].evolvedWaveCount;
        spawnWaves[baseWaveIndex].evolvedWaveIndex[count] = evolutionWaveIndex;
        spawnWaves[baseWaveIndex].evolvedWaveCount += 1;
        spawnWaves[evolutionWaveIndex].baseWaveIndex = baseWaveIndex;
        debug(spawnWaves[evolutionWaveIndex].spawnTriggerName+" now evolves from "+spawnWaves[baseWaveIndex].spawnTriggerName);
    }
    
    void RemoveSpawnWave(int index) {
        int i;
        for ( i = spawnWaves[index].count - 1; i >= 0; i = i - 1 ) {
            spawnWaveBudget += spawnWaves[index].value;
            if ( spawnWaves[index].waveTimer[i] != null ) {
                TimerPause(spawnWaves[index].waveTimer[i], true);
            }
        }
        if ( spawnWaves[index].spawnTrigger != null ) {
            TriggerDestroy(spawnWaves[index].spawnTrigger);
        }
        spawnWaves[index].count = 0;
    }
    
    void AddSpawnWave(int index) {
        int count;
        int numWaves;
        int baseWaveIndex = spawnWaves[index].baseWaveIndex;
        if ( baseWaveIndex >= 0 ) {
            // Is an evolveed, check if evolving...
            if ( spawnWaves[baseWaveIndex].baseWaveIndex == -1 ) {
                // Evolving a wave...
                spawnWaves[baseWaveIndex].baseWaveIndex = -2;
                numWaves = spawnWaves[baseWaveIndex].count;
                RemoveSpawnWave(baseWaveIndex);
                for ( count = 0; count < numWaves; count += 1 ) {
                    AddSpawnWave(index);
                }
                return;
            }
        }
        spawnWaveBudget -= spawnWaves[index].value;
        count = spawnWaves[index].count;
        if ( spawnWaves[index].waveTimer[count] == null ) {
            spawnWaves[index].waveTimer[count] = TimerCreate();
        }
        spawnWaves[index].count = spawnWaves[index].count + 1;
        Wait(RandomFixed(0,10.0), c_timeGame);
        TimerStart(spawnWaves[index].waveTimer[count], spawnWaves[index].frequency * RandomFixed(0.9,1.1), true, c_timeGame);
        if ( spawnWaves[index].spawnTrigger == null ) {
            spawnWaves[index].spawnTrigger = TriggerCreate(spawnWaves[index].spawnTriggerName);
        }
        TriggerExecute(spawnWaves[index].spawnTrigger, true, false);
        TriggerAddEventTimer(spawnWaves[index].spawnTrigger, spawnWaves[index].waveTimer[count]);
        debug("Added "+spawnWaves[index].spawnTriggerName+" "+IntToString(spawnWaves[index].count));
    }
    
    int SetRandomSpawnWaveChance(int index, int cost) {
        int baseChance = MAX_SPAWN_WAVE_DUPES - spawnWaves[index].count;
        fixed favor;
        if ( cost < spawnWaveBudget && baseChance > 0 ) {
            spawnWaves[index].chance = spawnWaves[index].value * baseChance;
        }
        debug(spawnWaves[index].spawnTriggerName+": "+IntToString(spawnWaves[index].chance));
        return spawnWaves[index].chance;
    }
    
    void AddRandomSpawnWave() {
        int count = 0;
        int baseWaveCost = 0;
        int evolvedWaveCost = 0;
        int chances = 0;
        int randomPick = 0;
        int i;
        int ii;
        int e;
        for ( i = 0; i < spawnWaveCount; i += 1 ) {
            spawnWaves[i].chance = 0;
        }
            
        for ( i = 0; i < spawnWaveCount; i += 1 ) {
            if ( spawnWaves[i].count == 0 ) {
                // Add a new wave (can only add base spawn waves that haven't evolved)
                if ( spawnWaves[i].baseWaveIndex == -1 ) {
                    chances += SetRandomSpawnWaveChance(i, spawnWaves[i].value);
                }
            } else {
                // Add to an existing base or evolved wave
                chances += SetRandomSpawnWaveChance(i, spawnWaves[i].value);
                
                // If unevolved base wave, add chance for evolved waves
                if ( spawnWaves[i].baseWaveIndex == -1 ) {
                    count = spawnWaves[i].count;
                    baseWaveCost = count * spawnWaves[i].value;
                    for ( ii = 0; ii < spawnWaves[i].evolvedWaveCount; ii += 1 ) {
                        e = spawnWaves[i].evolvedWaveIndex[ii];
                        evolvedWaveCost = count * spawnWaves[e].value;
                        chances += SetRandomSpawnWaveChance(e, evolvedWaveCost - baseWaveCost);
                    }
                }
            }
        }
        debug("Chances: "+IntToString(chances));
        if ( chances > 0 ) {
            randomPick = RandomInt(0, chances);
            debug("Random Pick: "+IntToString(randomPick));
            chances = 0;
            Wait(2, c_timeGame);
            for ( i = 0; i < spawnWaveCount; i += 1 ) {
                if ( spawnWaves[i].chance > 0 ) {
                    chances = chances + spawnWaves[i].chance;
                    debug("Checking: "+spawnWaves[i].spawnTriggerName+" "+IntToString(chances));
                    if ( randomPick < chances ) {
                        debug("Picking: "+spawnWaves[i].spawnTriggerName);
                        AddSpawnWave(i);
                        break;
                    }
                }
            }
        }
        debug("Remaining Budget: "+IntToString(spawnWaveBudget));
    }
    
    point GetRandomSpawnPointAir() {
        int index = RandomInt(0, MAX_SPAWN_POINTS - 1);
        if ( spawnPointsAir[index] == null ) {
            spawnPointsAir[index] = RegionRandomPoint(SPAWN_REGION_AIR);
        }
        return spawnPointsAir[index];
    }
    
    point GetRandomSpawnPointGround() {
        int index = RandomInt(0, MAX_SPAWN_POINTS - 1);
        if ( spawnPointsGround[index] == null ) {
            spawnPointsGround[index] = RegionRandomPoint(SPAWN_REGION_GROUND);
        }
        return spawnPointsGround[index];
    }
    
    void SpawnAttackingUnits(int count, string unitType, bool groundUnits) {
        unitgroup units;
        point spawnPoint;
        
        if ( groundUnits ) {
            spawnPoint = GetRandomSpawnPointGround();
        } else {
            spawnPoint = GetRandomSpawnPointAir();
        }
        
        units = UnitCreate(count, unitType, 0, SPAWN_OWNER, spawnPoint, 225.0);
        UnitGroupIssueOrder(units, OrderTargetingPoint(AbilityCommand("attack", 0), BASE_POINT), c_orderQueueAddToEnd);
    }
    
    bool BanelingWaveTrigger(bool testConditions, bool runActions) {
        SpawnAttackingUnits(2, "Baneling", true);
        return true;
    }
    
    bool HydraliskWaveTrigger(bool testConditions, bool runActions) {
        SpawnAttackingUnits(3, "Hydralisk", true);
        return true;
    }
    
    bool MutaliskWaveTrigger(bool testConditions, bool runActions) {
        SpawnAttackingUnits(3, "Mutalisk", false);
        return true;
    }
    
    bool RoachWaveTrigger(bool testConditions, bool runActions) {
        SpawnAttackingUnits(2, "Roach", true);
        return true;
    }
    
    bool UltraliskWaveTrigger(bool testConditions, bool runActions) {
        SpawnAttackingUnits(1, "Ultralisk", true);
        return true;
    }
    
    bool ZerglingWaveTrigger(bool testConditions, bool runActions) {
        SpawnAttackingUnits(5, "Zergling", true);
        return true;
    }
    
    bool AddSpawnWavesTrigger(bool testConditions, bool runActions) {
        waveCount += 1;
        spawnWaveBudget = spawnWaveBudget + (400 * waveCount);
        debug("Budget: "+IntToString(spawnWaveBudget));
        AddRandomSpawnWave();
        return true;
    }
    
    void initSpawnWaves() {
        timer t;
        int baseWaveIndex;
        int evolWaveIndex;
        
        baseWaveIndex = CreateSpawnWave(25.0, "RoachWaveTrigger",      240);
        baseWaveIndex = CreateSpawnWave(20.0, "BanelingWaveTrigger",   300);
        baseWaveIndex = CreateSpawnWave(25.0, "HydraliskWaveTrigger",  360);
        baseWaveIndex = CreateSpawnWave(15.0, "ZerglingWaveTrigger",   500);
        evolWaveIndex = CreateSpawnWave(15.0, "MutaliskWaveTrigger",  2400);
        AddSpawnWaveEvolution(baseWaveIndex, evolWaveIndex);
        evolWaveIndex = CreateSpawnWave(20.0, "UltraliskWaveTrigger", 1500);
        AddSpawnWaveEvolution(baseWaveIndex, evolWaveIndex);
        
        t = TimerCreate();
        TimerStart(t, 30, true, c_timeGame);
        TriggerAddEventTimer(TriggerCreate("AddSpawnWavesTrigger"), t);
        Wait(10.0, c_timeGame);
        AddRandomSpawnWave();
    }
    
    Posted in: Galaxy Scripting
  • 0

    posted a message on AI-Player to build structures in certain points or regions

    I'm a little late to the discussion here but to clarify a few things..

    Triggers: A trigger being enabled and on means that it's active. It still needs an event to "trigger" it - thus the name. If you want something to run once at the start of the game, use the Map Initialization event. If you want something to happen after a certain amount of time, use the Time Elapsed event. If you want something to happen periodically, you need to create a variable of type Timer and, in a separate trigger, use the Start Timer as Repeating timer action (this trigger will need its own event in order to run, of course). Then in another trigger, you can use the Timer Expired event.

    Of course, this is not what you want for your purposes, as you've discovered. Using the AI commands is the way to go for this.

    Enabled/Disabled Triggers: A trigger that's disabled won't even be part of the game - this is to let you remove triggers from the game without removing them from your source, in case you need to reference them or re-enable them later. Test triggers are a good use-case here as you can have them enabled while testing the map in order to give you special commands or privileges, then disable them before publishing the map.

    On/Off Triggers: A trigger that's marked as off is active in the code but will not trigger when its event occurs. Triggers can be turned on and off during the game. If you wanted to award player 1 with 50 minerals every 5 seconds for the first 5 minutes of the game, you could setup one trigger to award the minerals every 5 seconds then set another trigger that says to turn off the first trigger after 5 minutes.

    Create Unit Action: Also, to clarify, the Create Unit action does not order one unit to create another. The game will literally create the unit. It doesn't cost resources that way either. So had your trigger had a periodic event or some such thing to trigger it, the game would've created those units at those points (or near to them if they collided).

    Posted in: Triggers
  • 0

    posted a message on Defendable Walls

    That is so awesome! Any possibility of making a unit version of the walls. I'm trying to think of a game that could be made with them.. like one where players can build the wall and/or attack the walls.

    Posted in: Data Assets
  • 0

    posted a message on Weekly Terraining Exercise #149: Copypasta

    @joecab: Go

    Thanks for the feedback :)

    I wasn't aware of the sky box feature.. seems a lot of them don't work, but I found one that suited the map well. It's positioning was a bit difficult, I had to get the camera really low in order to make the skybox hit the skyline.. I think it made the shots more personal though.

    I also lessened the cloud per your advice, and I enlarged the mothership. :)

    Pics added to original post with the originals left for comparison.

    Posted in: Terrain
  • 0

    posted a message on Weekly Terraining Exercise #149: Copypasta

    I originally made this terrain for a small sandbox map a couple weeks ago with doodad placement similar to the zerg version. New to this stuff but I was really happy with it, and this seemed like a fun challenge to re-envision some new stories for my little world.

    Terran

    A Terran mining operation is making great progress in securing rich minerals to carter back to the homeworld. A small battalion of troops have setup defensive positions at the choke should any hostile forces happen to come across them.

    Zerg

    A Zerg Queen leads a small troop of zerglings on a perimiter patrol of their world, ever vigilant in protecting what remains of their dying race.

    Protoss

    Strange energy signals from a long abandoned Protoss colony have led a small contingent back to a place that brings back forgotten memories. But what could be causing these structures to emanate once again with life?

    Posted in: Terrain
  • 0

    posted a message on Creating an smooth loading screen transition

    Overview

    This tutorial is to show how you can make an attractive loading screen that's simple and transitions nicely into the actual game. Using this technique can help you to set player expectations of what they'll find once the game starts and help to build excitement during what is most likely the most boring part of playing any game.

    To demo, I created a small map that uses an overhead camera and swings down to the players view.

    Step 1

    Decide on what you want the player's first impression to be. Is it an eagle's eye view of the whole map (like in my demo) or a close up shot of a character that will present an in-game intro?

    Using the terrain editor, select the Cameras (C) palette and create a new camera that showcases your target material. For this tutorial, I named my camera Intro Camera, but name it whatever seems fitting to you.

    Step 2

    Create a trigger to apply the camera on game initialization.

    • Events:
      • Game - Map Initialization
    • Actions:
      • Camera - Apply Intro Camera for player 1 over 0.0 seconds with Existing Velocity% initial velocity, 10.0% deceleration, and Include Target
      • Camera - Lock camera input for player 1
      • Cinematics - Turn cinematic mode On for (All players) over Immediate seconds

    Replace Intro Camera with the name you gave your camera.

    If your map has more than one player, you can loop over the active players and replace "player 1" with "player Picked Payer" in the camera triggers above.

    Step 3

    Now that we have our intial view ready to go, we can get a screenshot for our loading screen.

    Ensure your settings are for high resolution (at least 1920 x 1080) if possible. Start your map up. It should apply your camera and do nothing else (you may need to temporarily disable any other triggers you have going). Now.. take a screenshot (the Print Screen button on your keyboard is an easy way to copy the screen to your clipboard).

    Using your preferred image editing software, open your screenshot or paste your clipboard to a new document. If it's not already, scale it to 1920 x 1080. This is the preferred size for loading screen images. Finally, save it as a TARGA (*.tga) file. This seems to be the preferred image format.

    Step 4

    Back in the Starcraft II Editor, open the Import (F9) window and import your new loading screen image.

    Once imported, you can navigate to Map > Map Loading Screen... in the menu and set it as your Image. While I haven't tested this beyond my demo, I believe Aspect Scaled would be the best Image Scaling setting.

    I also set Type to Custom and I set Body, Help, Help Restart, Subtitle, and Title all to a single empty space. This is why my screen is blank except for the loading indicator. It's up to you if you want text on the screen... many maps do you use it. I prefer a minimum of text.

    Step 5

    At this point if you start your map, your loading screen should look good and once the map does load there should be only a very subtle change between your image and the in-game view. There's one last part.. the transition.

    Back in the Triggers (F6) window, create 2 new triggers.

    • Events:
      • Timer - Elapsed time is 0.5 Game Time seconds
    • Actions:
      • Camera - Apply (Default game camera) for player 1 over 2.0 seconds with Existing Velocity% initial velocity, 10.0% deceleration, and Don't Include Target.
      • Camera - Pan the camera for player 1 to Player Start Location over 2.0 seconds with Existing Velocity% initial velocity, 10.0% deceleration, and Do Not use smart panning
      • Cinematics - Turn cinematic mode Off for (All players) over 2.0 seconds
    • Events:
      • Timer - Elapsed time is 2.5 Game Time seconds
    • Actions:
      • Camera - Unlock camera input for player 1

    I find that the 0.5 second wait on the first trigger adds a nice, albeit very brief, delay between the game loading and when the camera starts to reorient itself. I also find 2.0 seconds to be a small enough wait that I'm not bothered by long enough to enjoy the transition. Tweak values to your liking. :) The only real point of importance here is to not unlock the camera for your players until the full effect has finished (unless you really want to let them disrupt it, then by all means...).

    Like before, you can put the player triggers in a loop if you have multiple players and change Player Start Location to wherever their camera should be initially centered... likely on their unit or base or some such thing.

    Conclusion

    I put this together very quickly for my unit test map because it's a map I load often and I wanted something a little more pleasing to look at. Please feel free to leave comments/suggestions or point out any issues you run into. I do think too many maps rely on walls of text and dialogs to explain themselves to new users and it gets tiring so I hope we can see more creative use of the loading screen and more creative map introductions in the future. :)

    Oops

    Of course I read over the description a hundred times to make sure everything looks right before submitting, and I miss the typo in the subject. The one thing I can't fix. Can anyone help? "Creating a smooth loading screen transition

    Posted in: Tutorials
  • 0

    posted a message on Custom Heroes - How to Add Abilities

    (name change just to be confusing)

    @MasterWrath: Go I hope that's not the case. :)

    @SearingChicken: Go @GlornII: Go Thanks for the info. I'll try to get in touch with Kueken. :)

    @TyaStarcraft: Go Your maps are always so well polished. Just spent a half hour trying out Spellbreak. Super creative. And the ability system seems to do what I'm looking for... a single hotkey with changeable tooltip, icon, and ability. Do you have anything you could share? Map/mod source.. general instruction? :)

    Posted in: Triggers
  • To post a comment, please or register a new account.