Get Started With Lua Events Lesson 4: Examples and More Examples

From Scenario League Wiki
Revision as of 03:55, 11 January 2019 by Prof. Garfield (talk | contribs) (Added to lesson)
Jump to navigationJump to search

Back To Get Started With Lua Events

Get Started With Lua Events Lesson 4

We now know enough about how Lua works to start writing more events. We will again visit the Classic Rome scenario. Find the downoad put link here, whith the event file that I will start the lesson with. Your existing event file from put link to lesson 3 here is probably fine, however, as long as you debugged it properly.

Naming Objects

Why Should You Name Objects?

Most of the functions that are native to TOTPP operate on 'objects' like the unittype object or the city object. In order to get this object in the first place, it is necessary to use functions like civ.getUnitType or civ.getCity. It is recommended that we give objects names and use those names to refer to the object, rather than using civ.getNeededObject(integer) everywhere, or writing "wrapper" functions that can take integers as arguments.

There are several reasons for this recommendation. The first is that it is easier to understand what the code is doing. civ.createUnit(diplomat,romans,rome.location) is much easier to understand than civ.createUnit(civ.getUnitType(50),civ.getTribe(1),civ.getTile(40,26,0)). The second reason is that it is less error prone. Did you notice that the 50th unit in a standard game is not in fact a diplomat, but instead the explorer? Probably not. You are even less likely to realize that Rome is actually located at (40,28). This means you have to do more testing, to make sure every number is correct in your code. If you write the wrong number, the game will probably do something, just not the something you wanted. If you misspell a name, you are likely to get a nil error, and know where to fix the problem. So the second reason is that it is likely to reduce errors, and make it easier to find the errors that do occur.

A second reason to use names instead of integers is that it makes it easier to make changes to your code. Perhaps you originally had a game mechanic that used the meltdown feature of the Nuclear Plant, but then decided that the mechanic didn't work very well after all, and wish to replace it with a regular Power Plant instead. If you used the improvement number 21 in your code, you would have to search your code for every 21, determine if it refers to the Nuclear Plant, then change it to a 19 for the Power Plant instead. With a name, you would simply change NuclearPlant=civ.getImprovement(21) to NuclearPlant=civ.getImprovement(19) and all the events you wrote would continue to work as normal. If you chose to remove the NuclearPlant entirely from the game without replacing it, it is easier to search your code for NuclearPlant, and if you forget something, the game will usually give you an error because a nil value was received instead of an improvement object.

This is something that is actually likely to happen once your event file gets large, especially if you collaborate with others. (And you will have a large event file at some point. Since Lua will let you do almost anything, you will eventually try to do almost everything.) For example, in Over the Reich, unit type 109 was originally some kind of "munition" and was therefore to be deleted every turn. Eventually, we decided that munition wasn't used for very much, and we could use another munition in its place. Later, we used the 109 unit type slot for the "Historic Target" unit which was not supposed to be deleted with the munitions. However, the table governing the unit types to be deleted used integers, so I had to figure out why the unit was being deleted. This particular bug was made more difficult to sort out since sometimes the unit was supposed to be deleted.

Now that we've decided that we should give 'names' to hundreds of objects, we run into a problem. You can only have 200 local variables in a Lua file (module). Usually, it is good programming practice to split your program into multiple files before you have need of 200 local variables (and we'll learn how to do that in a latter lesson), but this doesn't really apply to our situation where we're storing data for our scenario. The solution is to use tables to store these kinds of values. So instead of

settler = civ.getUnitType(0)

We use

unitAliases = {}
unitAliases.settler = civ.getUnitType(0) 

Avoid using a table name that is likely to be a variable name in a function. For example

unit.settler = civ.getUnitType(0)

Would become a problem if we wrote the following loop:

for unit in civ.iterateUnits() do 
   if unit.type == unit.settler then
       civ.ui.text("We found a settler.")
   end
end 

In this case, "unit" is a very natural variable name for this for loop, but declaring a new local "unit" prevents us from accessing the "unit" table that was declared at a higher level. In Over the Reich, objectAliases was chosen as the form for most of these kinds of tables.

How to Name a City Object

For most objects that you would want to name (unit types, technologies, tribes) the corresponding function civ.getObject(id) is easy to use. At the time of writing this guide, it is inadvisible to try to name individual units, since the game will change their id number from time to time (to remove killed units). If it ever does become possible to name individual units, you will probably want to use someone else's code to do it anyway. But what about cities? Unlike unit types, you can't just count entries in the Rules.txt to find the id number.

One method is to use the in game console to find the id number of each city ( civ.getCurrentTile().city.id). This is the method used in Over the Reich, and, has not, thus far, led to any known problems in that scenario. However, there has been a report of a possible bug with this system, which at the time of writing hasn't been investigated further. This being the case, there are at least two other options that can be used (and which might be preferable anyway). The first is

cityAliases.rome = civ.getTile(40,28,0).city

Which is accessed by simply writing cityAliases.rome .

The second option is somewhat similar, except that it uses a function to get the city object at the time it is needed instead of storing the object in a table.

local function getCityAt(x,y,z)
   local function getCity()
       return civ.getTile(x,y,z).city
   end
   return getCity
end

Which is then used in the follwoing way:


local myCity = getCityAt(10,20,0)
-- Other Code
if city == myCity() then
-- code
end

In this method, the function getCityAt(10,20,0) generates another function, which gets the city at (10,20,0) or returns nil if no such city exists, and we call that new function when we want to actually get the city at (10,20,0). This method should not have any problems with cities being created or destroyed, so we'll use it for this tutorial. One disadvantage of the method is that we might not be able to re-use code designed for other kinds of objects. For example, if we have a function inTable(object,table) --> boolean which checks if the object is also a value in the table, we cannot use inTable(city,cityTable) and get true, since cityTable will have functions in it, not city objects. That is another reason for using the function method of getting cities in this tutorial: so we can see how code varies with how we get objects.

Exercise

In the Events.lua file for Classic Rome, take all the objects we've given names to thus far, and put them into tables. Use the tables tileAliases , unitAliases, tribeAliases, and cityAliases. Additionally, create a table parameters for numbers such as elephantCampaignCost, and textAliases for strings.

It is a good idea to give parameters names for similar reasons as for why you would like to name objects. There are two big reasons that deserve to be stated explicitly. The first is that it makes tweaking the program much easier. If you use parameter names everywhere, then by changing the definition at one place, you know that everything is changed, without searching all your code. The second is that you don't have to know the final parameter value while programming. As long as you know the name of the parameter, you can program, even if you've forgot or have not decided on the final value for the parameter. This is particularly useful if you are collaborating with someone else. You can program without waiting for confirmation on a value. Similar reasons apply for giving strings names.

Additionally, for everything being re-named, find where it is used, and re-name appropriately. You might find the replace functionality (in the search menu) of Notepad++ useful. If you are not using Notepad++, text editors usually have search and replace functionality.

Image 01

If you use the search and replace tool in Notepad++, make sure you check the boxes "Match Whole Word Only" and "Match Case" so you don't replace more than you expect.

Image 02

These are the names that I gave the objects and parameters in question. If you used different names, that is fine, but you will have to take that into account when copying code during this lesson. For Lesson 5, I will provide the events file that I finish with during this lesson.

Image 03

Note that I also created a terrainAliases table for terrain type numbers.

When changing names, perhaps you noticed that parameters.campaignCost isn't actually used anywhere (it was replaced with a campaign cost dependent on unit movement points.) You can use the search utility in your text editor to confirm this. You may delete the line if you wish.