63 - Ship AI
Normally I like to post dev blogs the day of a stream, but yesterday I streamed for 8 hours till 11pm, so I was pretty tired. So I guess we are going to go over what I did yesterday, and that's AI! So if you have only been reading the dev blogs and didn't know, I put out a public build last night! You can check it out by downloading a .zip file on my website, you do have to run an exe file, so if that's unsafe to you, you can instead watch a video of it on my youtube. So before code, let's go over how I did the AI, I think it was a clever trick. Basically it involves an order list that holds onto priorities of what needs to be done. Every so often the AI will run through this list and check if anything needs to be done. Each priority has two things, a filter of segments, and an order. The filter is for what needs to be done, so like manning a weapon is lower than fixing a broken part. The order is for in what order should crew members do things in, so for repairing, you want someone with a higher repair skill to go at it first. public enum Actions {RepairDisabledSystem, RepairSystem, Repair, ManWeapon }; public ShipAI(Ship ship) { Ship = ship; Priorites = new List(); Priorites.Add(Actions.RepairDisabledSystem); Priorites.Add(Actions.RepairSystem); Priorites.Add(Actions.Repair); Priorites.Add(Actions.ManWeapon); Segments = new List(); foreach (var item in Ship.Segments) { Segments.Add(item); } } If you don't know, an enum is a thing where you can basicly assign words to values, I'll be showing how it's used later. But under the hood RepairDisabledSystem = 0, RepairSystem = 1, and so on. While you very rarely need an enum, it can make things a lot cleaner. So in the constructor we are just getting a reference to the ship and adding the priorities to the list. There might be a way to do this more dynamically because as I said, each enum refers to a value, but that's for another day. Next we are just getting all the segments and making them into a list so it's easier to manage them. private void UpdatePriority() { Ship.DudeList.ForEach(x => x.Working = false); Movement = new Dictionary(); foreach (var item in Priorites) { if(item == Actions.RepairDisabledSystem) { if (DoStuff(Segments.Where(x => x.Disabled && x.System != null), x => x.Skills["Repair"], "repairDisabledSystem1")) break; if (DoStuff(Segments.Where(x => x.Disabled && x.System != null), x => x.Skills["Repair"], "repairDisabledSystem2")) break; } if (item == Actions.RepairSystem) { if (DoStuff(Segments.Where(x => x.HP < x.MaxHP && x.System != null), x => x.Skills["Repair"], "repairSystem1")) break; if (DoStuff(Segments.Where(x => x.HP < x.MaxHP && x.System != null), x => x.Skills["Repair"], "RepairSystem2")) break; } if (item == Actions.Repair) { if (DoStuff(Segments.Where(x => x.HP < x.MaxHP && x.System == null), x => x.Skills["Repair"], "Repair1")) break; if (DoStuff(Segments.Where(x => x.HP < x.MaxHP && x.System == null), x => x.Skills["Repair"], "Repair2")) break; } if (item == Actions.ManWeapon) if(DoStuff(Segments.Where(x => x.Disabled == false && x.System != null), x => x.Skills["Weapons"], "ManWeapon")) break; } Ship.MoveDudes(Movement); } Alright, here is the bulk of how the AI is ran. First we have to set everyone who is working to false just so we have the whole crew to work with. Then the Movement dictionary basically holds onto all the movement commands, at the end we will pass it to a movement method to be handled later. So the work happens in the foreach loop, because lists are ordered we know that in whatever order we added to the list that's the order the foreach loop will go in. Then we just check what enum is currently being used then we do some stuff. You might notice for the repairs I'm calling things twice, but for ManWeapon I only call it once. That's because two people can be fixing a part at the same time. Also this is where we needed to convert the 2d array into a list, all those where statements. You notice that on the first one it's checking for systems where it's disabled and it has a system. The order by part is done as a function, but basicly we are ordering things by skill level. private bool DoStuff(IEnumerable segments, Func stuff, string action) { bool allWorking = false; foreach (var item in segments) { var dude = Ship.DudeList.OrderByDescending(stuff).First(x => x.Working == false); dude.Working = true; Movement.Add(dude, item); if (AllDudesWorking()) { allWorking = true; break; } } return allWorking; } First of all, the string action is just a way for me to debug, you can safely ignore it. You can see we are passing in an IEnumerable so a collection of segments we can loop over, and a Func that lets us pass a lambda function. The bool allWorking is used to check if we have ran out of working dudes. Basicly if the number of workers is equal to the number of crew members we don't want to keep going, so se set that to true and break out of everything. That's what the if and break statements are for in the earlier method too. No reason to do more work then we need to. So when we loop over the filtered segments we first grab a dude with the highest skill level who isn't working. Then we set him to working, add movement, and check if everyone is working. That's more or less it for getting crew members to move around! Isn't that such a simple system? All I need to do to add another layer to the AI is add another value to the enum and create a filter and order, it's pretty simple and I'm happy with it! Now let's cover how I did AI for weapons. This one is pretty simple too, the AI is dumb, and it should be dumb. You will be fighting ship after ship, if the AI is too smart you will lose pretty fast, and that's not fun. We just call this method in the Weapons class when it's ready to shoot and it's not the player so we can set the target. private ShipPart PickTarget() { ShipPart part; var random = new System.Random(); if (random.Next(1) == 0) { var segs = new List(); foreach (var item in Ship.OtherShip.Segments) { if (item.isActiveAndEnabled) if (item.Disabled == false) { segs.Add(item); } } part = segs.ElementAt(random.Next(segs.Count)); } else { var connectors = new List(); for (int x = 0; x < Ship.OtherShip.SegmentSize; x++) { for (int y = 0; y < Ship.OtherShip.SegmentSize; y++) { if(x < Ship.OtherShip.SegmentSize - 1) { var connector = Ship.OtherShip.Segments[x, y].RightConnector; if (connector.isActiveAndEnabled) { connectors.Add(connector); } } if(y < Ship.OtherShip.SegmentSize -1) { var conn = Ship.OtherShip.Segments[x, y].UpConnector; if (conn.isActiveAndEnabled) connectors.Add(conn); } } } part = connectors.ElementAt(random.Next(connectors.Count)); } return part; } SO really this all boils down to picking a random segment or connector. We first more or less flip a coin with random.Next(1) == 0, random.Next(1) will return 0 or 1. Depending on that we just loop over every segment and connector so we can add it to a list. Then we are just calling thing.ElementAt(random.Next(thing.count), simple as that. I could of just explained this without showing code, but I wanted to show you guys how random works, because I don't think I've covered that. Well that's it for today. Stream should be happening soon and hopefully I'll write up a devblog for it tonight instead of tomorrow.
2/1/2017 1:56:37 PM

Add Comment Auther