Nearly Perfect Building Interaction
Author: jaminv (sc2mapster.com)Tags: data trigger  
Source: http://forums.sc2mapster.com/resour...Added 13 years ago 

The Problem
The ability type "Interact" is used to allow computer controlled building to sell items, units, upgrades, or whatever to all players. It's a great ability that allows you to do a great number of things with very little work. The problem is, this ability only works with neutral units. If the unit is anything other than neutral, you'll receive a message "cannot spend another player's resources". If you need the unit to be allied to certain players, this is a show stopper.

Some people have gotten around this by changing control of the building to the player who's interacting with it. This solution would not work for me, because I needed multiple people to be able to interact with the building at once. I also needed the building to act like an allied building in every way. I needed it to be destructible and say "our allies base is under attack".

On the finer details side of things, you can't see the queue on a building with interact, nor does it work with warp gates. The units pretty much have to come out instantly, or the player is confused. The queue is shared for each player, making it even less useful. There's also one rally point per building. Ability interact also doesn't tell the player that his unit is too far away; again, leading to confusion. I could probably live without most of those things, but I ended up finding ways around all of them.


The Path
A lot went into find the solution. I'm not going to bore you with the gory details, but a lot of the people in IRC helped me through this, and in the end I think the solution is very elegant. It's also not much code, which is great. I spent a great deal more time finding the solution and theorizing in IRC than I actually did programming anything.

Once I worked through all of that, I thought I should share my solution with the community. I know this is a common problem, and we are short on solutions. Feel free to use mine, and let me know if you find any better ways of doing it.


The Solution
So the way everything ended up working was this: I create a new "invisible" unit on top of the real unit. I do this once for every player I want to be able to interact with my shop building. Each copy is owned by the player that can interact with it. So they actually own it, and can interact with it normally. The clones are invulnerable, so that units don't attack them, but their life (and shields) are linked to the real building.

The invisible units themselves (called "fake" units pretty much everywhere in the code) are created in triggers. When you select the real unit, a trigger redirects your selection to the fake unit. Since you can only select one allied unit at a time, this behaves quite normally.

A behavior with a validator can tell if the player's hero unit is within a certain radius. The validator disables the behavior if there's no heroic unit within the specified radius. A requirement on the train ability checks for the behavior, and provides a nice message "Hero character must be within range". This works very nicely for the hero requirement, and allows the building to be hoykeyed, and the hotkey is retained but it can't be used out of range.

Finally, a simple trigger syncs the health and shields of the real buildings with the fake ones.

This solution meets all of my requirements and then some. Separate buildings for each units means separate queues/cooldowns and rally points. It seems a little weird, but it's really quite elegant in practice.


The Data
Behaviors
You'll need 3 behaviors, all buffs, that do nothing. I'm a big fan of editor prefixes (use them!), so I gave all of them the editor prefix "Building -". The behavior names I used were:

So go to the behaviors tab, Add Object..., and give it he name "Ally Interact". Change the behavior type to "Buff" and leave everything else as defaults. Hit OK. Change "Editor - Editor Prefix" to building. You don't need to change anything else about the behavior. Repeat these steps for the other two items on the list above.

These behaviors are all just place holders. They mark that a unit has a special property, but that special property is handled somewhere else. The Ally Interact behavior just like the interact ability. Just put it on a building you want to be able to interact with. Triggers handle the rest. The Fake Building behavior is set by our trigger when it creates the fake buildings. It's used in case we need to know later if the building is fake. Final, the Hero In Range behavior will actually be getting a validator shortly, to turn it on and off.

Why don't we dive right into that?

Validators
Jump to the validators tab and create a new object. I called mine "Hero in Range." Set the validator type should be "Enumerate Area". Leave everything else at default and hit OK. I then changed the editor prefix to "Building -".

This validator will return false if the criteria is met, and we actually want it to return true to disable our behavior. So we need to work a little backwards. We want to set it so that the criteria is that the hero is in range (thus the name). When the criteria are met, it returns false and doesn't disable the behavior. It's confusing, I know, but it's the way it works.

So click on Search - Areas and click the green X to add an area. Arc should be 360 already. Set Compare to "Greater Than", and Count to 0. Set Radius to whatever radius you want to use. I used 20 because I wanted a little room to move for my units. You may choose something different. Close the dialog by hitting OK.

Set Target - Location + to "Caster Unit". Set Validator - Compare to "Greater than". I don't know why you have to do it again there, but you do. Finally, set your Search - Search Filters. I set Heroic to "Required", and unchecked the boxes next to Ally, Enemy, and Neutral. Having only Player checked means it will only look for heroic units owner by the owner of the shop. Since each player has his/her own shop, this works perfectly.

The validator is complete. Go back to the behaviors tab, and find "Behavior - Hero In Range". Set the Behavior - Validators (Disable) to the validator you just created.

Requirements
The requirement does the real work, preventing the player from buying when the hero unit is out of range. Fortunately, it's also fairly simple. Create a new requirement. I called mine "Hero In Range" (for consistency). There's only one kind of requirement, so leave everything default and click OK. I then set the editor prefix to "Building -" (again).

Double click next to Requirements +, and then right click on the Use node, and click Add Requirement Node. Change the Type to "Count Behavior". Change the Alias to our behavior - "Building - Hero in Range". The State should be "Completed at Unit". Finally, set the Tooltip to "Hero character must be within range", or whatever you want it to say. Leave the Show node blank.

So now our requirement expects the building to have our behavior, otherwise we can't use whatever ability the requirement is attached to. The show node is blank, so it will show the ability no matter what, which is the behavior you expect. We also set the tooltip that will show up when the ability is greyed out, which is just plain good user interface.

One last note about requirements: you can only use one requirement per ability (at least with train abilities). If you're using an ability that already has a requirement, and you want to keep it, or if you want to use another ability, you'll need to combine the two requirements. Duplicate the pre-existing requirement, and add the same requirement we just set up to that requirement. It will now have both requirements.

Abilities
For the purposes of this tutorial, I will assume your ability already exists. It's most likely a train or upgrade ability. Either you've built it already, or you're using a pre-existing ability. Either way, I'm not going to go over the bazillions of ways an ability can be set up. If it doesn't exist already, stop now and make it.

If you need your ability for something else (like if you're going to make normal units of the same type), then duplicate the ability. If there are any actors, duplicate those with it. If you don't need to use the ability for anything else, you can just edit it as-is.

Both the ability types train and research have a section called Ability - Info. Double-click it. For each ability in this list, you need to add the requirement "Building - Hero In Range" to it. As I said before, if there's already a requirement that you want to use, you'll need to combine it with the hero in range requirement and use the combined requirement instead.

That's all you need to do for abilities.

Units
Finally, we get to our shop. Find whatever unit you're using as a shop, and add the following behaviors: "Building - Ally Interact" and "Building - Hero in Range". If you duplicated the ability, double click next to Ability - Abilities + and swap the old one for the duplicate. Make sure the command card buttons have the new ability as well.

The rest of the work is done in triggers.


The Triggers
This could all be done in a few less triggers, but I like to break things into small pieces so they're easy to deal with. I put all of the below code into a folder called "Buying"

Lets start with the most obvious piece, creating each fake building:

Action Definition - Create Fake Building
I am looking for a better way to make the building invisible, this method doesn't work well with buildings with small bases (like Pylons), because you can see the flat unit. When I find another method, I will edit the code here.

Create Fake Building
    Options: Action
    Return Type: (None)
    Parameters
        [color=#929292]// Player is the player to create the new unit for

        Player = 0 <Integer>
        // Building is the building to clone
        Building = No Unit <Unit>
    Grammar Text: Create Fake Building(, Player, Building)
    Hint Text: (None)
    Custom Script Code
    Local Variables
    Actions
        // Of course we need to create the unit first.
        // We use the type and position of the unit, and ignore placement.

        Unit - Create 1 (Unit type of Building) for player Player at (Position of Building) using default facing (Ignore Placement)
        // This is the method I've found works best to make the unit "invisible".
        // There are other ways, use whatever you find works best.
        // I may still change this at some point.
        // Scaling the Z to 10% makes the building quite flat, but doesn't affect the selection radius.

        Unit - Set (Last created unit) scale to (100.0%, 100.0%, 10.0%) of its original size
        // Setting the height to -999 makes the shadow go away.
        Unit - Change (Last created unit) height to -999.0 over 0.0 seconds
        // I don't want anybody shooting at my unit. They should shoot at the real unit instead.
        Unit - Make (Last created unit) Invulnerable
        // The fake building shouldn't have the ally interact behavior. It should only interact with it's owner.
        Unit - Remove 1 Building - Ally Interact from (Last created unit)
        // Instead we want to tag it as a fake building, in case we need to know that later.
        Unit - Add 1 Building - Fake Building to (Last created unit) from (Last created unit)[/color]

Hopefully the comments speak for themselves. This function creates one fake building. It will need to be called once for each fake building we create.

Initially, I built the fake buildings only when they were needed, but some weirdness with selection, and the fact that I decided to use this for Pylons, meant I needed to create all the fake buildings at the beginning of the game. This was done in another action definition:

Action Definition - Initialize Fake Units
Initialize Fake Units
    Options: Action
    Return Type: (None)
    Parameters
    Grammar Text: Initialize Fake Units()
    Hint Text: (None)
    Custom Script Code
    Local Variables
        [color=#929292]// Building is used for the for each unit loop (of type structure)

        Building = No Unit <Unit>
        // Ally is used for the for each player loop (who are allies of Building's owner)
        Ally = 0 <Integer>
    Actions
        // This action definition is to initialize all the "fake" units, which are really just invisible buildings stacked on top of computer controlled buildings.
        // When you click on the computer controlled building, a trigger switches your selection to the fake building that you own.
        // This function makes sure that building exists.
        // It is called by my main initialization function, at the very beginning of the game.
        // We're gonna search through all the units with type "Structure", and we're storing it in the variable Building.

        Unit Group - For each unit Building in (Any units in (Entire map) owned by player Any Player matching Required: Structure; Excluded: Missile, Dead, Hidden, with at most Any Amount) do (Actions)
            Actions
                // We need to check a couple of conditions about each unit
                General - If (Conditions) then do (Actions) else do (Actions)
                    If
                        And
                            Conditions
                                // The building needs to have our "Ally Interact" behavior
                                (Building has Building - Ally Interact) == true
                                // I have a pre-initialized player group that contains all the computer players.
                                // If you're using this for something else, use your own conditions here:

                                ((Owner of Building) is in Computers) == true
                    Then
                     // I need to create a "fake" building for each player allied with the owner of the building we've found
                        Player Group - For each player Ally in (Allies of player (Owner of Building)) do (Actions)
                            Actions
                                // We call another action definition to create the building.
                                // I'm not actually saving this unit to a variable, because I find it later by searching the map.
                                // This left the most flexibility, and didn't require a huge array for lots of units.

                                Create Fake Building(, Ally, Building)
                    Else[/color]

This action definition should be called by your map initialization trigger.

As you may have noticed in the comments, we don't actually save any of the units we create to variables. Why? Because I didn't feel that it would scale well. If you need hundred of buildings like this (and I did), the arrays would become unmanageable. Instead, I decided to search the map for any units of the same type owned by the player, within a very small radius. I put this into a function that returned the unit that was found.

Global Constant - Fake Building Check Radius
First though, lets create a global variable called "Fake Building Check Radius", and set the Type to "Real". Set the Initial Value to 0.5, and check the checkbox next to Constant. This way, we can change the radius quickly if we need to. We made it constant so we can be sure it won't accidentally be changed at any point in our code.

Function - Find Fake Building
Find Fake Building
    Options: Function
    Return Type: Unit
    Parameters
        [color=#929292]// Player is the player whose fake building we want to find

        Player = 0 <Integer>
        // Building is the real building for which we want to find its clone
        Building = No Unit <Unit>
    Grammar Text: Find Fake Building(, Player, Building)
    Hint Text: (None)
    Custom Script Code
    Local Variables
        // Search buildings is used to temporarily store all the units matching the search criteria. Only 1 unit should ever match the criteria.
        Search Buildings = (Empty unit group) <Unit Group>
    Actions
        // Search for all buildings within the Fake Building Check Radius, of the same type as the real building, owned by the selected player
        Variable - Set Search Buildings = ((Unit type of Building) units in (Region((Position of Building), Fake Building Check Radius)) owned by player Player matching Excluded: Missile, Dead, Hidden, with at most Any Amount)
        // This should only ever be one unit, so we're just going to return the first unit it the unit group
        General - Return (Unit 1 from Search Buildings)
[/color]

Now we need the trigger that switched the selection from the real unit to the fake one. This trigger is fairly simple:

Trigger - Select Shop
Select Shop
    Events
        [color=#929292]// Trigger on any unit being selected

        Unit Selection - Any Unit is Selected by player Any Player
    Local Variables
        // Building is the unit that was selected
        Building = (Triggering unit) <Unit>
    Conditions
        // We have several conditions that must be fulfilled
        And
            Conditions
                // The building needs to have our "Ally Interact" behavior
                (Building has Building - Ally Interact) == true
                // I require that the player be allied with the owner of the building
                ((Triggering player) is in (Allies of player (Owner of Building))) == true
                // I have a pre-initialized player group that contains all the computer players.
                // If you're using this for something else, use your own conditions here:

                ((Owner of Building) is in Computers) == true
    Actions
        // Clear the current selection
        Unit Selection - Deselect all units for player (Triggering player)
        // Find the fake unit and select it instead
        Unit Selection - Select (Find Fake Building(, (Triggering player), Building)) for player (Triggering player)[/color]
Finally, we need another trigger to sync the health of the fake buildings to the real ones:

Trigger - Sync Health
Sync Health
    Events
        [color=#929292]// Trigger on any unit life change

        Unit - Any Unit Life changes
        // or a change in shields
        Unit - Any Unit Shields changes
    Local Variables
        // Fake Unit is used in the for each unit loop (duplicates of triggering unit)
        Fake Unit = No Unit <Unit>
    Conditions
        // The triggering unit needs to have our "Ally Interact" behavior.
        ((Triggering unit) has Building - Ally Interact) == true
    Actions
        // Find each unit of the same type within our Fake Building Check Radius.
        Unit Group - For each unit Fake Unit in ((Unit type of (Triggering unit)) units in (Region((Position of (Triggering unit)), Fake Building Check Radius)) owned by player Any Player matching Excluded: Missile, Dead, Hidden, with at most Any Amount) do (Actions)
            Actions
                // And set the health and shields to those of the triggering unit.
                Unit - Set Fake Unit Life to ((Triggering unit) Life (Current))
                Unit - Set Fake Unit Shields to ((Triggering unit) Shields (Current))[/color]

That's all there is for the code.


The Conclusion
If everything went properly, you should have yourself some really nice-looking allied shops, that feel just like regular allied buildings.

Still, there are a couple elements I wish I could change. The consistency of the green selection circle with the yellow ring around it bugs me a little. I'd like it to be one or the other color, but not both. Really, I'd rather it be yellow all around, but the green does make it feel more like you can control the building. It's a minor inconvenience that I'm sure I can tolerate.

Also, there is a slight delay in the command card as you switch from the real building (with a blank command card, usually), to the fake building. I haven't been able to issue a command faster than it can switch command cards, but it is noticeable and it's not quite as smooth as if you're clicking from a normal building to another. Like I said, the interactivity is no different, but the appearance is a little off.

The following section should be removed soon

Currently, I am looking for a way to improve the invisibility. Although this one aspect isn't perfect, I still wanted to prepare this post while everything was fresh in my mind. When I find a better method, I will make the adjustments and remove this section.







Star Depot
Contact      Login