|
Post by arrrthritis on Nov 20, 2016 2:50:42 GMT
So, I'm mostly creating this thread as sort of a two way street for people who want to delve deeper into making a Tactics RPG, while at the same time hope to give people an avenue to call out my own bad coding habits. (+ it's better overall to find efficient solutions rather than patchwork solutions).
This OP is going to be a "Things to do/Solutions found" sort of list. Feel free to add suggestions of things to solve, or your own implementations of "solved" problems, and I will be sure to add them to the list (I hope this board allows people to edit their posts!)
Things to Do
[]Implement a Deployment phase []Implement different movement costs depending on tile type []Implement abilities depending on the tile type the unit is currently on. []Include Unit Shouts
Ability Effects to Implement
[]Pushing ability effects []Pulling ability effects []"On Unit Death" ability effects
Board Logic
[]Allow for units to traverse gaps in board geography []Allow for overlapping board terrain (gates, bridges, etc.)
Tasks Completed
[X]Implement Dynamic Unit Portraits [X]Modify map saving/loading to allow for more than one tile type [X]Create an ability range that doesn't allow units to target themselves.
It's very simple, but I can cover the Ability Ranges in this one post. I created a character with a whirling dervish sort of ability as a test- the unit swings their swords in a circle to damage every adjacent unit. With abilities as they are (and the AI as it is), this can often lead to the enemy AI moving to random squares and attacking itself for no good reason. I fixed this by creating a new ability range with ConstantAbilityRange as a general idea of where to start.
using UnityEngine; using System.Collections; using System.Collections.Generic;
public class AdjacentAbilityRange : AbilityRange {
public override List<Tile> GetTilesInRange(Board board) { List<Tile> tileList = board.Search(unit.tile, ExpandSearch);
tileList.Remove(unit.tile);
return tileList; }
bool ExpandSearch(Tile from, Tile to) { return (from.distance + 1) <= horizontal && Mathf.Abs(to.height - unit.tile.height) <= vertical; } }
It's pretty simple- do a search with the unit's tile as the start of the list and expand the range up to the horizontal limit (in this case, 1), and then remove the unit's tile from the result. Give this unit the FullAbilityArea script and he'll hit every tile adjacent to his position.
I applied this logic on a larger scale for abilities that I wanted to have a minimum range.
using UnityEngine; using System.Collections; using System.Collections.Generic;
public class MinimumAbilityRange : AbilityRange {
public int blindSpace;
public override List<Tile> GetTilesInRange(Board board){ List<Tile> tileList = board.Search(unit.tile, ExpandSearch); List<Tile> blindSpots = board.Search(unit.tile, BlindSearch);
foreach(Tile t in blindSpots) { tileList.Remove(t); } return tileList; }
bool ExpandSearch(Tile from, Tile to){ return (from.distance + 1) <= horizontal && Mathf.Abs(to.height - unit.tile.height) <= vertical; }
bool BlindSearch(Tile from, Tile to){ return (from.distance + 1) <= blindSpace && Mathf.Abs(to.height - unit.tile.height) <= vertical; } } This is more or less the same logic. I do two seperate searches, one with the outer bound and one with the inner bound (represented with blindSpace and BlindSearch). I do a board.Search with the outer bound and subtract the blindSpots list from the result I want to return.
So there we have it, ability ranges that (mostly) don't allow for AI Hari-Kiri. Throw the MinimumAbilityRange onto some designated artillery units and they'll seem more fragile than ever.
Right now i'm working on implementing some sort of pull ability (I played a lot of Roadhog in Overwatch). Tomorrow or Tuesday i'll go over how I modified the BoardCreator and Board to allow for different prefab usage.
|
|
|
Post by Admin on Nov 20, 2016 17:46:20 GMT
Great set of goals, I'll enjoy watching your progress. Clever solutions so far and a great use of the systems!
|
|
|
Post by arrrthritis on Nov 23, 2016 6:30:27 GMT
So for the map editor, I ended up making some very basic quality of life changes, while at the same time making some base changes to the Dictionary class in order to create a dictionary of points and strings in order to get the thing to serialize properly. While I wish I could take credit for making a serialized dictionary, I learned all about it here. It's definitely worth checking out. Overall, though, figuring out how to export different tiles from the editor was... moderately difficult before I found the serializable dictionary. I'll walk you through what I ended up doing to get the whole process sorted out. First off, I created the SerializableDictionary class I linked above. Then, I created a subclass with non-generic types within the file used to serialize the tiles themselves. using UnityEngine; using System.IO; using System.Collections; using System.Collections.Generic;
[System.Serializable] public class TileSkins : SerializableDictionary<Vector3, string> { }
public class LevelData : ScriptableObject{
public List<Vector3> tiles; public TileSkins tileSkins; }
As you can see above, within the LevelData script I created a new class called TileSkins, which is a Dictionary of points and strings. This is mostly going to be used in the BoardCreator and Board classes. Speaking of which, let's head on over to the BoardCreator and make some changes. First thing's first, I updated the Save function as follows. public void Save() {
string filePath = Application.dataPath + "/Resources/Levels";
if (!Directory.Exists(filePath))
CreateSaveDirectory();
LevelData board = ScriptableObject.CreateInstance<LevelData>();
board.tiles = new List<Vector3>(tiles.Count);
board.tileSkins = new TileSkins();
foreach (Tile t in tiles.Values){
Vector3 pos = new Vector3(t.pos.x, t.height, t.pos.y);
board.tiles.Add(pos);
string prefabName = t.name;
prefabName = prefabName.Substring(0, prefabName.Length - 7);
board.tileSkins.Add(pos, prefabName);
}
string fileName = string.Format("Assets/Resources/Levels/{1}.asset", filePath, levelName);
AssetDatabase.CreateAsset(board, fileName);
}
It's very similar to the code from the tutorial, the only difference is that I instantiate a new TileSkins dictionary, and then for every tile that I add to the Tiles list, I also add to the tileSkins dictionary. Because every prefab that I created with the board had the name GrassB(Clone), I truncated the prefab names by seven characters to remove that clone bit. (I also added a serializeable field where I could input what I wanted to name the level when I save it, which is where levelName comes into play). You'll know your serialization worked if you have your list of tiles with the name Tile Skins underneath it. public void Load() {
Clear();
if (levelData == null)
return;
foreach (Vector3 v in levelData.tiles) {
string prefabName;
levelData.tileSkins.TryGetValue(v, out prefabName);
GameObject variableForPrefab = (GameObject)Resources.Load("Prefabs/Blocks/" + prefabName, typeof(GameObject));
GameObject instance = Instantiate(variableForPrefab) as GameObject;
Tile t = instance.GetComponent<Tile>();
t.Load(v);
tiles.Add(t.pos, t);
}
}
And with that, we go into the Board class and modify our Load() function to account for our new TileSkins data. public void Load (LevelData data) {
_min = new Point(int.MaxValue, int.MaxValue);
_max = new Point(int.MinValue, int.MinValue);
for (int i = 0; i < data.tiles.Count; ++i) {
string prefabName;
data.tileSkins.TryGetValue(data.tiles[i], out prefabName);
GameObject variableForPrefab = (GameObject)Resources.Load("Prefabs/Blocks/" + prefabName, typeof(GameObject));
GameObject instance = Instantiate(variableForPrefab) as GameObject;
Tile t = instance.GetComponent<Tile>();
t.Load(data.tiles[i]);
tiles.Add(t.pos, t);
_min.x = Mathf.Min(_min.x, t.pos.x);
_min.y = Mathf.Min(_min.y, t.pos.y);
_max.x = Mathf.Max(_max.x, t.pos.x);
_max.y = Mathf.Max(_max.y, t.pos.y);
}
} (I also removed the SerializedField for the tile prefab used in the game). I don't feel too good about using Resources.Load (I'll probably have to change that to an AssetPackage, as soon as I figure out how to do those), but right now it seems to get the job done just fine. I mostly do this because I want to differentiate the tiles by more than just their texture. I might want to attach a script to the prefab that designates what kind of tile it is- that might affect movement, evasion, or allow me to introduce unique mechanics to a geomancer-like class. Don't quote me on that part, yet, because there's still a bunch of stuff I need to implement otherwise. The game ends up looking something like this with my prefabs (apologies for the wonky resolution. I took the screenshot in the unity editor.) This whole process ended up being the first thing I did since I finished the tutorial, and it was a bit of a doozy figuring it out. But in the end I feel... well, I'm sure the code could be better, but it's acceptable for now.
|
|
|
Post by Admin on Nov 23, 2016 14:06:31 GMT
Another good post. Thanks for the link, I bet many users would be super excited to see a serialized dictionary in Unity.
Why do you not feel good about Resources.Load? I use it all the time in all of my games. As long as you remember to unload your resource after you are finished with it, it is a convenient tool and good enough for the job at hand. I'm assuming you meant to compare it to an AssetBundle rather than an AssetPackage - are you intending to make a web game?
|
|
|
Post by Aka on Jun 6, 2018 20:50:54 GMT
Arrrrrthritis, oh my god. You are amazing and a lifesaver. I was chewing my fingers off trying to sort this out. I have some other ideas to try, but this is brilliant. Well done!
|
|
aka
New Member
Posts: 5
|
Post by aka on Jun 7, 2018 0:30:07 GMT
I feel as though I may have jumped the gun a little - I'm getting some errors and lack of functionality.
_min and _max are displaying errors about not existing in the current context. I believe I've done everything correctly, but clearly that isn't the case. Is it possible to elaborate more on how exactly the SerializableDictionary should be implemented? I made a new C# script with "SerializableDictionary" as the filename, and copied the script from your source. It's not derived from monobehaviour so I know it's not supposed to be attached to anything, and I can see that it's being referenced in your modified LevelData.cs. Given that, I modified the other scripts (board and boardcreator) as you suggested, but as I stated above, _min and _max returned errors. what have I missed?
|
|
|
Post by Admin on Jun 7, 2018 2:24:07 GMT
You may need to re-read over his post to make sure you put things in the right place. The variables "_min" and "_max" are defined in the "Board" script, so if you copied his method somewhere else like the "BoardCreator" by mistake then you might get this problem.
|
|
aka
New Member
Posts: 5
|
Post by aka on Jun 7, 2018 11:04:54 GMT
I've checked and rechecked, and everything seems to be in its proper place. I'm at work now and don't have access to my files at home, and what I'd like to do is link some screenshots... but that will have to wait. I hate to waste your time on trivial issues Regarding this statement: You'll know your serialization worked if you have your list of tiles with the name Tile Skins underneath it. What exactly should I be seeing? The parser in my brain is an older version, haha.
|
|
|
Post by Admin on Jun 7, 2018 18:26:22 GMT
I followed along a bit to see if it would work for me... the only note I missed at first was to add a serialized string "levelName" to the "BoardCreator", otherwise everything compiles fine for me. When you look at the finished result of a level data asset, it will look pretty much exactly like it did before, except that you will also see the label "Tile Skins" beneath the "Tiles" array. Note that it wont show the prefab used per tile location in the inspector, it simply shows that there is in fact a serialized object there.
Keep in mind that if you struggle with this too much, that there are plenty of other ways you could approach it. For example, you could make the LevelData to be a list of a new type of serialized struct that already holds the position as well as the prefab name in a single object. Something like this:
[System.Serializable] public struct TileData { public Vector3 position; public string prefabName; }
... and as an added bonus, you can actually see the prefab name in the inspector this way.
|
|
aka
New Member
Posts: 5
|
Post by aka on Jun 7, 2018 22:22:41 GMT
Well, there's good news and bad news!
The good news is that I got it all working.
The bad news is that it wasn't working because I didn't think to actually swap out the tile prefab to other kinds of prefabs with a proper directory pointer. lol x 50000. The load function also doesn't load the tiles as childed to the boardcreator anymore either, but I'm sure that's something I can figure out.
Thank you very kindly for your help. This is "childhood dream" level stuff for me, and I'm grateful for the shared knowledge.
|
|
|
Post by Admin on Jun 7, 2018 22:52:24 GMT
That is awesome, I am glad to help you achieve your goals!
|
|
|
Post by 7thsage on Jun 11, 2018 7:06:16 GMT
I've been slowly working through the tutorial and finally made it through the pathfinding section. There have been some things in the past I've wanted to eventually tackle changing, but have been afraid of messing with something will cause difficulties later on in the series, but with the pathfinding, I think the changes I'm doing will hopefully not cascade into the rest of the series too much. Currently I'm working on getting the teleporting and flying units to go over gaps, I've got it pretty much working. Still need to get the cursor to go over those squares as well. I'm sure its not the most elegant solution, but so far it seems to work. Anyway, I thought I'd share what I've got so far, and if anyone has comments or suggestions, I'd love to hear them.
The first change I made was to the Tile.cs file so I could keep track of which tiles are empty
[HideInInspector] public bool isEmpty;
The biggest changes are in Board.cs
for (int i = 0; i < 4; ++i)
{
Tile next = GetTile(t.pos + dirs[i]);
if (next == null)
{
GameObject instance = Instantiate(tilePrefab) as GameObject;
instance.SetActive(false);
next = instance.GetComponent<Tile>();
next.pos = t.pos + dirs[i];
next.isEmpty = true;
next.distance = int.MaxValue;
}
if (next.distance <= t.distance + 1)
continue;
if (addTile(t, next))
{
next.distance = t.distance + 1;
next.prev = t;
checkNext.Enqueue(next);
retValue.Add(next);
}
}
In Movement.cs
protected virtual bool ExpandSearch(Tile from, Tile to)
{
if (to == null || to.isEmpty == true)
return false;
return ( from.distance + 1 ) <= range;
}
protected virtual void Filter(List<Tile> tiles)
{
for (int i = tiles.Count - 1; i >= 0; --i)
{
if (tiles[i] == null || tiles[i].isEmpty == true)
{
tiles.RemoveAt(i);
}
else if (tiles[i].content != null)
tiles.RemoveAt(i);
}
}
And finally in FlyMovement.cs and TelportMovement.cs I added
protected override bool ExpandSearch(Tile from, Tile to)
{
return ( from.distance + 1 ) <= range;
} I think those are all the changes I made? Like I said, I still don't have the tile selector working, but I'll probably tackle that next. After that I want to make a couple more tweaks so walking units can jump or small gaps whether they are empty tiles or just significantly lower.
|
|
|
Post by Admin on Jun 11, 2018 14:38:21 GMT
Those sound like some fun challenges, I hope it goes well for you! One approach I had considered was to redesign the boards so that all the tiles (even gaps) are present, but perhaps you add components to the gap tiles that make sure units can't land on them (much like an occupied tile), but that flying and teleporting units can traverse past them. It would also solve the issue with the cursor since you can move it to any tile regardless of if it is occupied. Either way it sounds like you are making progress, so thats great!
|
|
|
Post by 7thsage on Jun 15, 2018 5:18:40 GMT
The changes to make the tile go over empty spaces turned out simpler than I expected. In the future I still need to add a function to limit the indicator to the board extents, but for now it works.
In BattleState.cs
protected virtual void SelectTile(Point p)
{
if (pos == p)
return;
Vector3 indicatorPosition;
pos = p;
if (!board.tiles.ContainsKey(p))
indicatorPosition = new Vector3(p.x, 0, p.y);
else
indicatorPosition = board.tiles[p].center;
tileSelectionIndicator.localPosition = indicatorPosition;
} And to make sure I don't get an error when trying to select over an empty tile In SelectUnitState.cs(at the beginning of the OnFire method)
if (owner.currentTile == null)
return;
|
|
|
Post by zeatech on Oct 25, 2018 18:53:03 GMT
First off, thanks to arrrthritis for sharing.
I have a question around the "different tiles solution here". Instead of using a dictionairy to "bolt" on the skins on the position, could you just add more properties to the Tile object, like texture, friction (if you want slower movement on some tiles), and potentially references to prefabs like trees that should be on it.. and then in LevelData save the whole Tile object instead of just a Vector3 with the position of the tile?.
I guess the Tile would also have to hold its own position so that its saved along with it.
As a quick proof of concept i created a class called TileMetadata:
public class TileMetadata { public Vector3 pos { get; set; } public int height { get; set; } public Material material { get; set; } }
Which i used in leveldata instead of the list of vector3s. Then i updated the save/load methods in boardcreator.cs and tile.cs to save/load the material along with the tiles position.
Any thoughts? Is this something to keep building on?
|
|