Lab 9: Collector Robot

Objective

In lab 9, you will create a robot that wanders around the map looking for loose items like gold or diamond and collecting them. If the entity takes any damage (either by wandering into a bad situation, getting attacked by other entities, or getting attacked by other players), it drops all of its items and stumbles around for twenty seconds until it has recovered enough to continue collecting stuff. While it's dazed, nearby players then have a chance to grab the collector bot's loot before it comes to its senses.

Notes

To Spawn An Entity In Game

To Add a New Task with a Specific Priority

void tasks.addTask (int priority, EntityAIBase task)

To Get A List<Entity> Of All Entities In An Entity's World

List<Entity> entityList = (List<Entity>) entity.worldObj.getLoadedEntityList();

Each item in this list is of type Entity.

Robot Navigation (Pathfinding)

entity.getNavigator().tryMoveToEntityLiving(Entity other, float speed);

For the float speed parameter, use Robot.SPEED_FAST, Robot.SPEED_NORMAL, or Robot.SPEED_SLOW. entity.getNavigator().clearPathEntity(); // Stop robot navigation

Getting Distance Between Two Entities

float distance = entity1.getDistanceToEntity(entity2);

Adding & Removing Items In The World

void dropItem (Item item, int amount); item.worldObj.removeEntity(EntityItem item);

Managing EntityItem's and ItemStack.

ItemStack itemStack = entityItem.getEntityItem(); int amount = itemStack.stackSize; Item item = itemStack.getItem(); String printableItemName = item.getUnlocalizedName(); // For Debugging

Hash Map Methods

boolean hashMap.contains(key); // Checks for existence of a key void hashMap.put (key, value); // Adds an entry ValueType hashMap.remove (key); // Removes an entry ValueType hashMap.get (key); // Just examines an entry // Constructing HashMaps HashMap<KeyType,ValueType> hashMap = new HashMap<KeyType,ValueType>(); Set<KeyType> keySet = hashMap.keySet(); // Getting a set of all keys in a hash map // Examining the contents of a hash map for (KeyType key : hashMap.keySet()) { ValueType value = hashMap.get(key); } void hashMap.clear(); // Clears a hash map

Robot Damage Events

public void Robot.onEntityDamage (DamageSource source, float amount);

This method is called whenever the robot receives any kind of damage. You won't care about the type or amount of damage, but you will drop the robot's inventory and start a stun cycle whenever it happens.

System Time

long System.currentTimeMillis()

The above method gets the number of milliseconds since January 1, 1970. You can use this value to figure out the current time. For our purposes, where we need elapsed time, you will only care about the difference between timestamps.

For example: long timeThreeSecondsFromNow = (long)(3.0 * 1000) + System.currentTimeMillis(); boolean threeSecondsElapsed = System.currentTimeMillis() > timeThreeSecondsFromNow;

Tasks

Part 1 — Stub In The New CollectorBot Entity

  1. Start by creating a new class called CollectorBot that extends the Robot class. Implement the default constructor that takes in a net.minecraft.world.World object as its parameter. Call the super constructor with that world parameter.
  2. In the constructor, add the EntityAIWander task with priority 1. When you create the EntityAIWander task, pass in this new collector bot and a speed of SPEED_NORMAL.
  3. Add the new robot to the EntitiesModule class. Register the CollectorBot with a name of "collector", and a texture of "gold_robot".
  4. Start up the game and issue the command "/robot collector". You should see a gold-colored robot that wanders around the map and does nothing else.

Part 2 — Stub In A New EntityAICollectLoot Task

  1. Create a new class called EntityAICollectLoot that extends EntityAIBase.
  2. The constructor takes a CollectorBot and a float search radius. Store both of these values in member variables of the new task.
  3. Override the methods public boolean shouldExecute() and public boolean continueExecuting(). The collection task will run all the time at first, so both of these methods should just return true.
  4. Stub in the override method public void updateTask(). For now, just return immediately.
  5. In the CollectorBot constructor, create a new EntityAICollectLoot task. The collect-loot task's constructor will take the new collector bot's reference and a search radius. Use a search radius of 10. You'll need to have access to this task later on, so store a reference to it in a local class member variable. Finally, add this task with priority 2.
  6. Verify your code so far by running the game and issuing the command "/robot collector". You should see a gold-colored robot that still just wanders around the map and does nothing else (since the EntityAICollectLoot task doesn't yet do anything).

Part 3 — Implement Basic CollectorBot Functionality

Now it's time to implement the basic loot searching and collection task. We want the robot to find the nearest piece of loot (net.minecraft.entity.item.EntityItem) within its search radius, make its way to that item, and then pick it up.

  1. In the EntityAICollectLoot task class, we now need to implement the first phase of updateTask(). To begin with, we'll first need a task class variable to track the currently targeted piece of loot: call it targetLoot. Loot items wil be of type EntityItem. Initialize it to null to begin with.
  2. The updateTask() method has two parts: in the first part, the task is to choose a piece of loot if we don't currently have one targeted. In the second part, when we have a piece of loot chosen, the task is to move to and collect that item.
  3. Create a function to select the next piece of loot. It will only be used by the EntityAICollectLoot task, so it should be private. It will return the selected EntityItem, and doesn't need any arguments. Call it selectNewTarget.
  4. In the selectNewTarget function, get a list of all entities in the collector bot's world. You will then iterate through this list, and find the entity that matches the following criteria:

    1. it must be an instance of EntityItem,
    2. it must not be marked "dead" (item.isDead), and
    3. it must be within the search radius of the collector bot's location.
    4. it must be the single closest entity

    (Note that items are marked dead when when they are scheduled to be deleted or are destroyed by another entity.)

  5. If you find an item that matches the above criteria, then set that item as the target loot, and return the new target.
  6. At the beginning of updateTask(), if we don't currently have a target loot item, get a new piece of target loot using the selectNewTarget() function. If we couldn't find a new target item that meets our criteria, then return from updateTask().
  7. After we've made sure that we have an item as our target loot, check to see if anything has marked it as dead. If so, stop the robot's navigation, set targetLoot to null, and return from updateTask.
  8. Move towards the loot target at normal speed.
  9. If the collector bot is within a distance of 1.5 from the target loot, then it can pick it up. Stop the robot navigation, remove the target loot from the world, and set the targetLoot to null.
  10. Test out the collector bot. To do this, load items in your inventory that you can drop for the collector bot to find. When you bring up the inventory in game, you can click on an item multiple times to create a "stack" of items (you'll see a count over the selected item). Drag this stack of items to an inventory slot, and hit escape to get back to the game. In game, select the item slot you want, and then drop these items by pressing the 'q' key. Now spawn the collector bot with the "/robot collector" command. It should run around and gather up the items you dropped.

Part 4 — Track items in the robot's inventory

Up to now, the robot really just removes items from the world; it just looks like it's been picking them up. Now we need to actually keep track of the items that the robot has gathered. We'll need this for the next part, where the robot drops all the items it has collected.

  1. The first task is to add an item inventory to the CollectorBot. We'll use a HashMap<Item,Integer> for this. Individual items will be the keys for this inventory, and the values will be the total count for each item. Create a class member variable for this (name it inventory), and initialize it to a new HashMap.
  2. Add a new CollectorBot method with the following signature: public void addItem (EntityItem entityItem)

  3. In the new addItem function, as outlined in the notes section, you'll get the item stack from the given entityItem. From that, you'll get the item and item count. Compute the new total count for that item type. You will need to handle the case where the robot currently has no items of that type, and the case where it already has some number of those items. Once you've computed the new total, put that entry in the inventory hash map.
  4. It may help to create a small function to print the current robot inventory (using System.out.printf()) as you develop this part. See the notes section for a way to examine the contents of a hash map, and the tip for getting a printableItemName for a given item. You can use this for your inventory print routine, which you can then call every time you add a new item.
  5. Now that you have a way to add and track items to the robot's inventory, we need to have the EntityAICollectLoot task call this method when we pick up new loot. Do this in updateTask(), when we determine that we're within range of our target loot. Add the call to robot.addItem when we pick up the target loot.
  6. Is everything implemented correctly? Run the game to find out. Make sure that the robot picks up items. If you implemented the debug print function, verify that the inventory is growing as you expect.

Part 5 — Drop all items when the robot is damaged

Now we'll add the behavior that the robot drops all of its inventory whenever it receives any damage.

  1. First, we'll provide an override of the CollectorBot's onEntityDamage method. The signature for this method is public void onEntityDamage (DamageSource source, float amount);

    The implementation of this method is simple: just call a new CollectorBot method dropInventory() with no arguments.

  2. Now we need to implement private void dropInventory(). This method needs to iterate through all items in its inventory, and drop the correct count of each item. See the notes section for dropItem. Once all items have been dropped, clear the robot's inventory. The notes section has the information you need on how to clear a hash map.
  3. After dropping the robot's inventory, and if you implemented the printInventory debug method, you may want to call this to verify that nothing is left in the robot's inventory.
  4. Time to test. Run the game, toss out a bunch of items for the robot to collect, and then smack it. You will probably see that the robot turns red when you hit it, but otherwise nothing else will look different. In particular, there won't be a bunch of objects laying around the robot. What happened?
  5. The CollectorBot is very industrious. Even if it properly dropped all of its inventory on the ground, the EntityAICollectLoot task is still running, so it will just immediately think "look at all this loot!", and pick it right back up. You can keep thwacking CollectorBot upside the head, and it will keep dropping and immediately picking back up all of its loot until it keels over, keeping everything it gathered. Now what?

Part 6 — CollectorBot stops collecting items when damaged

The key to making CollectorBot more useful is adding a delay before it picks up all items it dropped. We'll have the robot wander around stunned for a while before the its loot collection task resumes. That will complete our CollectorBot implementation.

  1. In EntityAICollectLoot, we're going to add support for suspending the collection task for a period of time. We'll be modifying the shouldExecute and continueExecuting tasks to do this. Before we do that, we'll need a new private long field named suspendEnt before we start. Initialize it to zero to start with. This field will store the time at which task suspension is over.
  2. Reference the notes section to review system time methods.
  3. Add a new method with the following signature: public void suspend (float suspendSeconds)

    This method will take the specified number of seconds and the current time in milliseconds, and compute the time when the task suspension should end (also in milliseconds). Store that result in suspendEnd.

  4. If suspendEnd is zero, then we are not suspended. Given that, update the continueExecuting method to return true only if we're not currently suspended.
  5. Update the shouldExecute method. Given the current system time and the suspendEnd variable, determine if we're still in the suspend time period. If we are still suspended, then return false (we should not execute the task). If we are either not suspended or are now past the suspension time period, set suspendEnd to zero to indicate that the collection task is not suspended, and return true (we should execute our main task).
  6. Back in the CollectorBot class, and using the code we just wrote above, suspend the collection task for 20 seconds when we take any damage.
  7. Time for the final test. Run the game, and feed the collector bot a bunch of items. Now smack the bot (from a distance), and you should notice that it drops all of its loot, and doesn't pick it back up. (Note that if you're too close, your player will end up picking up the items, which is why you want a bit of distance before striking the robot.) Wait 20 seconds, and verify that the robot starts picking things up again.

Part 7 — Fit & Finish

Take a look at all of your code. You've added a bunch, changed a bunch, and probably wrestled a bit with different parts. How can you make the code cleaner? Easier to read? Are you using magic numbers where you should instead have used class constants? Is your code well documented? Review the program logic and see if you can accomplish the same tasks in simpler ways. How much code can you simplify or reduce? Re-order your methods so that reviewers and other programmers can follow the logic easier. Are there places where you could use a variable to hold a common result? Are your variable names clear? These are just a few things to look at, but make a huge difference in your final result.