Skip to content

Commit 79ce6af

Browse files
authored
Merge pull request #122 from C7-Game/InitialCombat
Threaded arch, animations, and combat
2 parents cc9c7db + 8fc49a9 commit 79ce6af

24 files changed

+1077
-562
lines changed

C7/AnimationTracker.cs

Lines changed: 79 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -1,109 +1,118 @@
11

2-
// The purpose of the AnimationTracker is to store the state of all ongoing animations in a module separate from the rest of the UI. It doesn't do
3-
// anything with the animations, it simply keeps record of them while they're playing then calls a callback function when they're done. So it's
4-
// basically just a stopwatch. Its update function must be called regularly so it can follow the passage of time, right now this is done in the Game
5-
// class's _Process method. There is one instance of AnimationTracker and it is located in Game. TODO: Consider moving it to MapView.
6-
7-
// The callbacks are hopefully temporary. I don't like using them since they obscure control flow as they get called at some later time potentially by
8-
// a different thread. The threading issue doesn't matter at the moment since everything important runs on one thread but this could change if we want
9-
// to have separate UI and engine threads (as I believe we should).
10-
112
using System;
123
using System.Collections.Generic;
4+
using System.Threading;
135
using System.Linq;
146
using C7GameData;
15-
using C7Engine; // for IAnimationControl, OnAnimationCompleted
7+
using C7Engine;
168

17-
public class AnimationTracker : IAnimationControl {
18-
public static readonly OnAnimationCompleted doNothing = (unitGUID, action) => { return true; };
9+
public class AnimationTracker {
10+
private Civ3AnimData civ3AnimData;
1911

20-
private Civ3UnitAnim civ3UnitAnim;
21-
22-
public AnimationTracker(Civ3UnitAnim civ3UnitAnim)
12+
public AnimationTracker(Civ3AnimData civ3AnimData)
2313
{
24-
this.civ3UnitAnim = civ3UnitAnim;
14+
this.civ3AnimData = civ3AnimData;
2515
}
2616

2717
public struct ActiveAnimation {
2818
public long startTimeMS, endTimeMS;
29-
public MapUnit.AnimatedAction action;
30-
public OnAnimationCompleted callback;
19+
public AutoResetEvent completionEvent;
20+
public Civ3Anim anim;
3121
}
3222

33-
private Dictionary<string, ActiveAnimation> activeAnims = new Dictionary<string, ActiveAnimation>();
34-
private Dictionary<string, ActiveAnimation> completedAnims = new Dictionary<string, ActiveAnimation>();
23+
private Dictionary<string, ActiveAnimation> activeAnims = new Dictionary<string, ActiveAnimation>();
3524

3625
public long getCurrentTimeMS()
3726
{
3827
return DateTime.Now.Ticks / TimeSpan.TicksPerMillisecond;
3928
}
4029

41-
public void startAnimation(MapUnit unit, MapUnit.AnimatedAction action, OnAnimationCompleted callback)
30+
private string getTileID(Tile tile)
31+
{
32+
// Generate a string to ID this tile that won't conflict with the unit GUIDs. TODO: Eventually we'll implement a common way of ID'ing
33+
// all game objects. Use that here instead.
34+
return String.Format("Tile.{0}.{1}", tile.xCoordinate, tile.yCoordinate);
35+
}
36+
37+
private void startAnimation(string id, Civ3Anim anim, AutoResetEvent completionEvent)
4238
{
4339
long currentTimeMS = getCurrentTimeMS();
44-
long animDurationMS = (long)(1000.0 * civ3UnitAnim.getDuration(unit.unitType.name, action));
40+
long animDurationMS = (long)(1000.0 * anim.getDuration());
4541

4642
ActiveAnimation aa;
47-
if (activeAnims.TryGetValue(unit.guid, out aa)) {
43+
if (activeAnims.TryGetValue(id, out aa)) {
4844
// If there's already an animation playing for this unit, end it first before replacing it
49-
aa.callback(unit.guid, aa.action);
45+
// TODO: Consider instead queueing up the new animation until after the first one is completed
46+
if (aa.completionEvent != null)
47+
aa.completionEvent.Set();
5048
}
51-
aa = new ActiveAnimation { startTimeMS = currentTimeMS, endTimeMS = currentTimeMS + animDurationMS, action = action, callback = callback ?? doNothing };
49+
aa = new ActiveAnimation { startTimeMS = currentTimeMS, endTimeMS = currentTimeMS + animDurationMS, completionEvent = completionEvent, anim = anim };
5250

53-
civ3UnitAnim.playSound(unit.unitType.name, action);
51+
anim.playSound();
5452

55-
activeAnims[unit.guid] = aa;
56-
completedAnims.Remove(unit.guid);
53+
activeAnims[id] = aa;
5754
}
5855

59-
public void endAnimation(MapUnit unit, bool triggerCallback = true)
56+
public void startAnimation(MapUnit unit, MapUnit.AnimatedAction action, AutoResetEvent completionEvent)
57+
{
58+
startAnimation(unit.guid, civ3AnimData.forUnit(unit.unitType.name, action), completionEvent);
59+
}
60+
61+
public void startAnimation(Tile tile, AnimatedEffect effect, AutoResetEvent completionEvent)
62+
{
63+
startAnimation(getTileID(tile), civ3AnimData.forEffect(effect), completionEvent);
64+
}
65+
66+
public void endAnimation(MapUnit unit)
6067
{
6168
ActiveAnimation aa;
62-
if (triggerCallback && activeAnims.TryGetValue(unit.guid, out aa)) {
63-
var forget = aa.callback(unit.guid, aa.action);
64-
if (! forget)
65-
completedAnims[unit.guid] = aa;
69+
if (activeAnims.TryGetValue(unit.guid, out aa)) {
70+
if (aa.completionEvent != null)
71+
aa.completionEvent.Set();
6672
activeAnims.Remove(unit.guid);
67-
} else {
68-
activeAnims .Remove(unit.guid);
69-
completedAnims.Remove(unit.guid);
7073
}
7174
}
7275

7376
public bool hasCurrentAction(MapUnit unit)
7477
{
75-
return activeAnims.ContainsKey(unit.guid) || completedAnims.ContainsKey(unit.guid);
78+
return activeAnims.ContainsKey(unit.guid);
7679
}
7780

78-
public (MapUnit.AnimatedAction, double) getCurrentActionAndRepetitionCount(MapUnit unit)
81+
public (MapUnit.AnimatedAction, double) getCurrentActionAndRepetitionCount(string id)
7982
{
80-
ActiveAnimation aa;
81-
if (! activeAnims.TryGetValue(unit.guid, out aa))
82-
aa = completedAnims[unit.guid];
83-
83+
ActiveAnimation aa = activeAnims[id];
8484
var durationMS = (double)(aa.endTimeMS - aa.startTimeMS);
8585
if (durationMS <= 0.0)
8686
durationMS = 1.0;
8787
var repCount = (double)(getCurrentTimeMS() - aa.startTimeMS) / durationMS;
88-
return (aa.action, repCount);
88+
return (aa.anim.action, repCount);
89+
}
90+
91+
public (MapUnit.AnimatedAction, double) getCurrentActionAndRepetitionCount(MapUnit unit)
92+
{
93+
return getCurrentActionAndRepetitionCount(unit.guid);
94+
}
95+
96+
public (MapUnit.AnimatedAction, double) getCurrentActionAndRepetitionCount(Tile tile)
97+
{
98+
return getCurrentActionAndRepetitionCount(getTileID(tile));
8999
}
90100

91101
public void update()
92102
{
93103
long currentTimeMS = getCurrentTimeMS();
94104
var keysToRemove = new List<string>();
95105
foreach (var guidAAPair in activeAnims.Where(guidAAPair => guidAAPair.Value.endTimeMS <= currentTimeMS)) {
96-
var (unitGUID, aa) = (guidAAPair.Key, guidAAPair.Value);
97-
var forget = aa.callback(unitGUID, aa.action);
98-
if (! forget)
99-
completedAnims[unitGUID] = aa;
100-
keysToRemove.Add(unitGUID);
106+
var (id, aa) = (guidAAPair.Key, guidAAPair.Value);
107+
if (aa.completionEvent != null)
108+
aa.completionEvent.Set();
109+
keysToRemove.Add(id);
101110
}
102111
foreach (var key in keysToRemove)
103112
activeAnims.Remove(key);
104113
}
105114

106-
public MapUnit.ActiveAnimation getActiveAnimation(MapUnit unit)
115+
public MapUnit.Appearance getUnitAppearance(MapUnit unit)
107116
{
108117
if (hasCurrentAction(unit)) {
109118
var (action, repCount) = getCurrentActionAndRepetitionCount(unit);
@@ -128,20 +137,29 @@ public MapUnit.ActiveAnimation getActiveAnimation(MapUnit unit)
128137
offsetY = -1 * dY * (1f - progress);
129138
}
130139

131-
return new MapUnit.ActiveAnimation {
140+
return new MapUnit.Appearance {
132141
action = action,
133-
direction = unit.facingDirection,
134-
progress = progress,
135-
offsetX = offsetX,
136-
offsetY = offsetY
137-
};
142+
direction = unit.facingDirection,
143+
progress = progress,
144+
offsetX = offsetX,
145+
offsetY = offsetY
146+
};
138147
} else
139-
return new MapUnit.ActiveAnimation {
148+
return new MapUnit.Appearance {
140149
action = unit.isFortified ? MapUnit.AnimatedAction.FORTIFY : MapUnit.AnimatedAction.DEFAULT,
141-
direction = unit.facingDirection,
142-
progress = 1f,
143-
offsetX = 0f,
144-
offsetY = 0f
145-
};
150+
direction = unit.facingDirection,
151+
progress = 1f,
152+
offsetX = 0f,
153+
offsetY = 0f
154+
};
155+
}
156+
157+
public Civ3Anim getTileEffect(Tile tile)
158+
{
159+
ActiveAnimation aa;
160+
if (activeAnims.TryGetValue(getTileID(tile), out aa))
161+
return aa.anim;
162+
else
163+
return null;
146164
}
147165
}

C7/Civ3AnimData.cs

Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
2+
// Civ3AnimData's purpose is to store the data associated with Civ 3 animations, for example the contents of each folder in Art/Units. It does lazy
3+
// loading & memoization so each file is loaded only when it's needed and then stored so it is only ever loaded once per game. The main (and only)
4+
// instance of Civ3AnimData is kept in Game, AnimationTracker holds a reference to it.
5+
6+
// It would be nice to load this data close to where it's used, e.g., have UnitLayer load the FlicSheets, instead of putting all the loading code in
7+
// one detached class like this. That's how things originally worked but I created Civ3AnimData to solve two issues:
8+
// 1. AnimationTracker and UnitLayer both need to load the unit INIs. So we either have a common place to store the INIs or duplication of work, and I
9+
// think the former is the better choice.
10+
// 2. AnimationTracker needs to know the duration of animations, which awkwardly cannot be determined based on the INI files alone. In order to know
11+
// the duration of an anim you must know how many frames it has, and the only way to know that is to read its flic file.
12+
13+
// The intended usage is to access the animation data through a Civ3Anim object obtained through the "forUnit" or "forEffect" methods. For example:
14+
// civ3AnimData.forUnit("Warrior", MapUnit.AnimatedAction.FORTIFY).playSound()
15+
// To play the warrior's foritfy sound effect.
16+
17+
using System;
18+
using System.Collections.Generic;
19+
using Godot;
20+
using IniParser;
21+
using IniParser.Model;
22+
using C7GameData;
23+
24+
public class Civ3AnimData
25+
{
26+
private AudioStreamPlayer audioPlayer;
27+
28+
public Civ3AnimData(AudioStreamPlayer audioPlayer)
29+
{
30+
this.audioPlayer = audioPlayer;
31+
}
32+
33+
private Dictionary<string, IniData> iniDatas = new Dictionary<string, IniData>();
34+
35+
public IniData getINIData(string pathKey)
36+
{
37+
IniData tr;
38+
if (! iniDatas.TryGetValue(pathKey, out tr)) {
39+
string fullPath = Util.Civ3MediaPath(pathKey);
40+
tr = (new FileIniDataParser()).ReadFile(fullPath);
41+
iniDatas.Add(pathKey, tr);
42+
}
43+
return tr;
44+
}
45+
46+
public IniData getUnitINIData(string unitTypeName)
47+
{
48+
return getINIData(String.Format("Art/Units/{0}/{0}.INI", unitTypeName));
49+
}
50+
51+
// Looks up the name of the flic file associated with a given action in an animation INI. If there is no flic file listed for the action,
52+
// returns instead the file name for the default action, and if that's missing too, throws an exception.
53+
public string getFlicFileName(IniData iniData, MapUnit.AnimatedAction action)
54+
{
55+
string fileName = iniData["Animations"][action.ToString()];
56+
if ((fileName != null) && (fileName != ""))
57+
return fileName;
58+
else if (action != MapUnit.AnimatedAction.DEFAULT)
59+
return getFlicFileName(iniData, MapUnit.AnimatedAction.DEFAULT);
60+
else
61+
throw new Exception("Missing default animation"); // TODO: Add the INI's file name to the error message
62+
}
63+
64+
private Dictionary<string, Util.FlicSheet> flicSheets = new Dictionary<string, Util.FlicSheet>();
65+
66+
public Util.FlicSheet getFlicSheet(string rootPath, IniData iniData, MapUnit.AnimatedAction action)
67+
{
68+
Util.FlicSheet tr;
69+
string pathKey = rootPath + "/" + getFlicFileName(iniData, action);
70+
if (! flicSheets.TryGetValue(pathKey, out tr)) {
71+
(tr, _) = Util.loadFlicSheet(pathKey);
72+
flicSheets.Add(pathKey, tr);
73+
}
74+
return tr;
75+
}
76+
77+
private Dictionary<string, AudioStreamSample> wavs = new Dictionary<string, AudioStreamSample>();
78+
79+
public void playSound(string rootPath, IniData iniData, MapUnit.AnimatedAction action)
80+
{
81+
string fileName = iniData["Sound Effects"][action.ToString()];
82+
if (fileName.EndsWith(".WAV", StringComparison.CurrentCultureIgnoreCase)) {
83+
AudioStreamSample wav;
84+
var pathKey = rootPath + "/" + fileName;
85+
if (! wavs.TryGetValue(pathKey, out wav)) {
86+
wav = Util.LoadWAVFromDisk(Util.Civ3MediaPath(pathKey));
87+
wavs.Add(pathKey, wav);
88+
}
89+
audioPlayer.Stream = wav;
90+
audioPlayer.Play();
91+
}
92+
}
93+
94+
public Civ3Anim forUnit(string unitTypeName, MapUnit.AnimatedAction action)
95+
{
96+
return new Civ3Anim(this, unitTypeName, action);
97+
}
98+
99+
public Civ3Anim forEffect(AnimatedEffect effect)
100+
{
101+
return new Civ3Anim(this, effect);
102+
}
103+
}
104+
105+
public class Civ3Anim
106+
{
107+
public Civ3AnimData civ3AnimData { get; private set; }
108+
public string folderPath { get; private set; } // For example "Art/Units/Warrior" or "Art/Animations/Trajectory"
109+
public string iniFileName { get; private set; }
110+
public MapUnit.AnimatedAction action { get; private set; }
111+
112+
public Civ3Anim(Civ3AnimData civ3AnimData, string unitTypeName, MapUnit.AnimatedAction action)
113+
{
114+
this.civ3AnimData = civ3AnimData;
115+
this.folderPath = "Art/Units/" + unitTypeName;
116+
this.iniFileName = unitTypeName + ".ini";
117+
this.action = action;
118+
}
119+
120+
public static readonly Dictionary<AnimatedEffect, string> effectCategories = new Dictionary<AnimatedEffect, string>
121+
{
122+
{ AnimatedEffect.Hit , "Trajectory" },
123+
{ AnimatedEffect.Hit2 , "Trajectory" },
124+
{ AnimatedEffect.Hit3 , "Trajectory" },
125+
{ AnimatedEffect.Hit5 , "Trajectory" },
126+
{ AnimatedEffect.Miss , "Trajectory" },
127+
{ AnimatedEffect.WaterMiss, "Trajectory" }
128+
};
129+
130+
public static readonly Dictionary<AnimatedEffect, string> effectINIFileNames = new Dictionary<AnimatedEffect, string>
131+
{
132+
{ AnimatedEffect.Hit , "hit.ini" },
133+
{ AnimatedEffect.Hit2 , "hit2.ini" },
134+
{ AnimatedEffect.Hit3 , "hit3.ini" },
135+
{ AnimatedEffect.Hit5 , "hit5.ini" },
136+
{ AnimatedEffect.Miss , "miss.ini" },
137+
{ AnimatedEffect.WaterMiss, "water miss.ini" }
138+
};
139+
140+
public Civ3Anim(Civ3AnimData civ3AnimData, AnimatedEffect effect)
141+
{
142+
this.civ3AnimData = civ3AnimData;
143+
this.folderPath = "Art/Animations/" + effectCategories[effect];
144+
this.iniFileName = effectINIFileNames[effect];
145+
this.action = MapUnit.AnimatedAction.DEATH;
146+
}
147+
148+
public IniData getINIData()
149+
{
150+
return civ3AnimData.getINIData(folderPath + "/" + iniFileName);
151+
}
152+
153+
public Util.FlicSheet getFlicSheet()
154+
{
155+
return civ3AnimData.getFlicSheet(folderPath, getINIData(), action);
156+
}
157+
158+
public void playSound()
159+
{
160+
civ3AnimData.playSound(folderPath, getINIData(), action);
161+
}
162+
163+
public double getDuration()
164+
{
165+
Util.FlicSheet flicSheet = getFlicSheet();
166+
double frameCount = flicSheet.indices.GetWidth() / flicSheet.spriteWidth;
167+
return frameCount / 20.0; // Civ 3 anims often run at 20 FPS TODO: Do they all? How could we tell? Is it exactly 20 FPS?
168+
}
169+
}

0 commit comments

Comments
 (0)