User:Knorke/Sandbox3

From Spring
Jump to navigationJump to search

FIXME: remove off-site links for images once uploading works again http://springrts.com/mantis/view.php?id=4612


This text talks about how to put features onto the map, using Lua.

The text can be read in two ways:

  • As a Lua tutorial that expands on Lua:Tutorial_GettingStarted and FIXME: that other page in sandbox
  • For mappers as way to put features onto their map.
    • In this case you can skip some of the "finer details" and just copypaste what seems interessting.


To follow the examples you can either...

  • ...use your WIP map in .sdd format
  • ...or unpack an existing map
  • ...or use the test-mutator example from previous tutorial.

This text will be based around the map "DesertTriad"

Creating a feature: Spring.CreateFeature

Again we use our test-hill in the bottom right corner, of which we already know the coordinates:

We want to create a feature on this hill.

Create a file: LuaRules\gadgets\MyLuaFeatureTesting.lua

At Lua_SyncedCtrl#Feature_Handling we find the function Spring.CreateFeature

We need the parameters: featurename and x,y,z coordinates. The other parameters do not matter for now.

The map "DesertTriad" already includes some rock features that we could use. If using the test-mutator then we can also use whatever features the game includes, usually that is mostly wrecks of destroyed units.

Another group of features that we can use are the ones hardcoded in the engine: Geovents and trees.

We use a feature named "treetype0", it is a tree that comes with Spring engine.

Putting feature name and coordinates as parameters:

Spring.CreateFeature ("treetype0", 3830, 390, 3600)

GameStart() or Initialize() or GamePreload() ?

Where should we put the CreateFeature code?

There are three callin functions that appear suitable:

GameStart() Initialize() GamePreload() - can callin can I take?

  • GameStart() is called as soon as the game begins.
    • However, it is not yet called when the players are ingame but have not readied up yet.
    • => If this callin was used then features would not appear until the game is started. Thus players could not see while choosing their start positions etc.
  • GamePreload() is called as soon as soon the "game is ingame."
    • Features spawned in this function will be visible before readying up.
    • However, it is only called once: Right after the game has finished loading.
      • That means if we want to test our Lua by typing /luarules reload then it will not trigger again.
  • Initialize() is called when the gadget is initialized.
    • Features spawned in this function will be visible before readying up.
    • When typing /luarules reload the gadgets get initialized again and the function gets called again.
      • This makes it suitable for testing.

=> Both Initialize() and GamePreload() can be used to spawn features without "delay."

=> Maybe GamePreload() is a bit "more technical correct" but Initialize() is more suited for testing, so we use that.


complete gadget that plants a tree

Thus the gadget now looks like this:

function gadget:GetInfo()
  return {
    name      = "lua map stuff",
    desc      = "create some features",
    author    = "youe name",
    date      = "the day a lua was made",
    license   = "GNU GPL, v2",
    layer     = 0,
    enabled   = true,
  }
end

if not gadgetHandler:IsSyncedCode() then

function gadget:Initialize()
	Spring.CreateFeature ("treetype0", 3830,  390, 3600)
end

else --unsynced part below

end

The result:

[File:onetreeonhillpcs0m.jpg]

Tip
Such simple code is really all it takes to put features on a map.

If one wanted one could now replaces the featurename and coordinates and use it to populate a map. In fact some maps do just that.

Tip
If you need to remove the tree for testing purposes, then reclaim it with the Commander or a constructor. (We will learn a better way soon)


If a tree falls from the sky... does it make a sound?

One interessting thing is that Spring physics do not get activated until the game actually starts.

Why is that relevant for spawning features?

If we use a too high y-coordinate (vertical axis / height) then the tree will hover in the air until the game starts.

[File:featuresfallatgamestaaesnm.gif]

And then it will suddendly fall down! (Look at above gif and watch the small countdown)

Similiar if we use too low y-coordinate then the tree will be created underground and only "pop" above ground once the game starts!

Tip
It is important to spawn features (and units) at the correct y-coordinate.

GetGroundHeight

Instead of reading the ground height from the tooltip we can also use the function Spring.GetGroundHeight:

function gadget:Initialize()
	x = 3830
	z = 3600
	y = Spring.GetGroundHeight (x,z)
	Spring.CreateFeature ("treetype0", x,y,z)
end


Functional forest

We can put our tree-spawner code into a function and call it from Initialize() multiple times with different coordinates:

function PlantTree (x,z)
	y = Spring.GetGroundHeight (x,z)
	Spring.CreateFeature ("treetype0", x,y,z)
end

function gadget:Initialize()
	PlantTree (3830,3600)
	PlantTree (3850,3650)
	PlantTree (3800,3650)
	PlantTree (3810,320)
end

Result:

[File:treesspawnedonhill74shj.jpg]

Tip
For practice try to make a function that spawns geothermal spots! The featurename is "geovent".

Deleting all features from map

Sometimes one might want to test the spawning of features again & again, on a clean plate. The function below deletes all features on the map:

function DestroyAllFeatures ()
	local features = Spring.GetAllFeatures ()
	for i in pairs(features) do
		Spring.DestroyFeature (features[i])
	end
end

It can be called in Initialize() before we spawn our features, to ensure we always test on an empty map.

When packing the map for release, one might want to comment it out again.

A forest is many, many, many trees

A handful of trees is often not enough for a map. Let's turn the whole bottom left corner of the map into a forest.

But planting a whole forest by typing coordinates of many trees seems very cumbersome?

What about randomizing the placement of the tree?

We look up the coordinates of that area: x-coordinate we want 3500 to 4000, for z-coordinate we want 3000 to 4000.

...and put it into a random number generator function.

x = math.random (3500,4000)
z = math.random (3000,4000)
PlantTree (x,z)

We want to plant many trees. Instead of copy-pasting those lines 100 times, we use a loop:

function gadget:Initialize()
	DestroyAllFeatures ()
	for i=1,100 do
		x = math.random (3500,4000)
		z = math.random (3000,4000)
		PlantTree (x,z)
	end
end

The result:

[File:treesspawnedrandomlyf3snt.jpg]

Tip
Forests are perfect for such random placement because the excact position of an indiviual tree does not matter much.

Spawning features in patterns

With a little creativity one can quickly fill the map in interessting ways.

For example try to create a function that spawns small clusters of trees.

Or imagine a function that spawns a big mineral crystal, surrounded by smaller rocks.

Or imagine an ore-mining facility with pieces of ore spread around it.

etc

Below is some examples that are hopefully understandable without too lenghty explainations.

Lua rocks

function Rock (x,z)
	local featureName = "pdrock" .. math.random (1, 4)
	Spring.CreateFeature (featureName, x,Spring.GetGroundHeight (x,z),z, math.random (0,32000))
end

function rockCircle (x,z, radius, numberOfRocks)
for d = 0,360, 360/numberOfRocks do
		local x = x + (math.sin (math.rad(d)) * radius)
		local z = z + (math.cos (math.rad(d)) * radius)
		Rock (x, z)
	end
end

Example usage:

rockCircle (2000,2000, 240, 25)

Result:

File:CircleOfRocks.jpg [File:circle_of_rockso5stt.jpg]

  • Note 1: local featureName = "pdrock" .. math.random (1, 4)

The rock features included in map "DesertTriad" are named "pdrock1" to "pdrock4". So we take "pdrock" and append a random number in range of 1 to 4 to that base name.

  • Note 2: The heading-parameter is used here too, randomized. The heading (rotation) of features is probally not measured 0 to 32000, but for this case that does not matter. If the number is too high it will simply "wrap around."


In my maps I have seen rings of smoke through the trees...

...and the voices of those who stand looking.

function TreeSpiral (x,z)
	for d = 0,720, 20 do
		local x = x + (math.sin (math.rad(d)) * d/2)
		local z = z + (math.cos (math.rad(d)) * d/2)
		PlantTree (x,z)
	end
	y = Spring.GetGroundHeight (x,z)
	Spring.CreateFeature ("geovent", x,y,z)
end

Example usage:

TreeSpiral (2000,2000)

Result:

A geothermal vent in the middle of a spiral of trees.

[File:treespiralwithgeoventu1swc.jpg]

Typing so many coordinates sucks!

We saw how some typing could be saved by using loops or random placement. But surely there must be another way?

We could try to save the coordinates of all features of the map into a file.

If we wanted to do that, we would preferably save the coordinates in a way that can also be read back by another script. For now that seems too difficult so we think of another way.

  • think * think * think *

Coordinates Helper widget

Spring allows players to mark tactical locations on the map by holding ~ or ^ and clicking the mousewheel.

That might also be useful for marking locations of features, if we can just save those coordinates?

Let's see if we can create a small helper widget for that purpose.

Create a file LuaUI\widgets\MapMarkerCoordinatesLogger.lua


MapDrawCmd: 1,2,3 testing!

For start let's just see if we can print the coordinates of a map marker to the console.

We discover the callin: MapDrawCmd(playerID, cmdType, px, py, pz, label)

function widget:GetInfo()
  return {
    name      = "MapMarkerCoordinatesLogger",
    desc      = "prints map marker coordinates",
    author    = "this lame tutorial or something",
    date      = "9 Nov 2010",
    license   = "GNU GPL, v2",
    layer     = -5,
    enabled   = true,
  }
end

function widget:MapDrawCmd(playerID, cmdType, px, py, pz, label)
	Spring.Echo (px, py,pz)
end

Result:

[File:mapmarkercoordinatestnwsng.jpg]

We see that the coordinates of the markers get printed to chat. (and thus to infolog.txt, too)

Not bad, but could be less bad.

Turning test into something useful

One obvious issue is that it does not just log placing markers but also outputs numbers when drawing lines or deleting.

Let's upgrade the widget so that:

  1. It only logs placing markers but not erasing or lines.
  2. instead of coordinates it outputs a whole function call like PlantTree (3830,3600)
  3. writes to a file instead of chat
  4. The functionname that gets written should be changeable from within game
  5. It has a cooler name

{{Tip|Before reading onwards: Try figure out a solution by yourself!

1) Sadly the wiki does not tell us how we know the difference between marker/lines/erasing. We must figure it out!

MapDrawCmd(playerID, cmdType, px, py, pz, label)

The paremter cmdType looks interessting. Let's just print that one too:

Spring.Echo (px, py,pz, cmdType)

We see that cmdType has the value "point" when placing markers and other values when doing other things.

So if cmdType is of wrong value we just return and do nothing:

function widget:MapDrawCmd(playerID, cmdType, px, py, pz, label)
	if cmdType ~= "point" then return end
...


2) Creating the complete function call like "PlantTree (3830,3600)" is just a matter of appending some strings:

local functionName = "PlantTree"
local s = functionName .. " (" .. px ..", " .. pz .. ")"
Spring.Echo (s)

Note that we only save the X and Z coordinates because the Y-coordinate we can later get anyway with Spring.GetGroundHeight

3) About writing to file we can find many tutorials on web if we google something like "Lua write to file" or similiar.

The result:

local file = io.open("myCoordinatesFile.txt", "a")
file:write(s)
file:flush()
file:close()

4) Markers can have a label text attached to them. Let's use that to allow user to change the functionname.

if label ~= "" then functionName = label end

5) Since the widget writes points into files for features placement, we name it: Pifffp


Complete Pifffp widget

Putting everything together we get:

function widget:GetInfo()
  return {
    name      = "Pifffp aka MapMarkerCoordinatesLogger",
    desc      = "prints Lua functions for placing featues",
    author    = "this lame tutorial or something",
    date      = "today",
    license   = "GNU GPL, v2",
    layer     = -5,
    enabled   = true,
  }
end

local functionName = "PlantTree"
function widget:MapDrawCmd(playerID, cmdType, px, py, pz, label)
	if cmdType ~= "point" then return end
	if label ~= "" then functionName = label end
	--Spring.Echo (px, py,pz, cmdType, "-"..label.."-")
	
	local s = functionName .. " (" .. px ..", " .. pz .. ")"
	Spring.Echo (s)
	
	local file = io.open("myCoordinatesFile.txt", "a")
	file:write(s .. "\n")
	file:flush()
	file:close()
end


Using the Pifffp tool as a user/mapper

Activate the widget in F11 list.

Now start placing map markers. Every time you place a marker the coordinates get logged into myCoordinatesFile.txt, already formated as parameters to a function call.

(The file should usually be written next to spring.exe)

To change the name of the function place a marker with a label text. All folling markers will then use that text, until a new label text is used.

Example usage:

[File:pifffp_markers7lsq1.jpg]

Example output:

PlantTree (1977, 3569)
PlantTree (1933, 3372)
PlantTree (1901, 3489)
PlantTree (1850, 3612)
PlantTree (2008, 3643)
PlantTree (2101, 3497)
PlantTree (2112, 3637)
PlantTree (2228, 3605)
PlantTree (2226, 3464)
PlantTree (2043, 3406)
PlantTree (2163, 3357)
PlantTree (2293, 3278)
PlantTree (2125, 3179)
PlantTree (1986, 3193)
Rock (1526, 3099)
Rock (1576, 3024)
Rock (1500, 2926)
Rock (1373, 3035)
Rock (1438, 3212)
Rock (1556, 3264)
Rock (1699, 3122)
Rock (1670, 2919)
Rock (1398, 2918)
Geovent (1818, 3298)
rockCircle (2308, 2788)

We recognize these functions from the beginning of the tutorial.

Hopefully still have them ready in our LuaRules\gadgets\MyLuaFeatureTesting.lua file so that we only need to paste the Pifffp output into Initialize

(I you are making a real map you might want to use a better filename like "spawnMyFeatures.lua")

Note that for the function rockCircle we still need to fill in the parameters radius and amount of rocks like so:

rockCircle (2308, 2788, 200, 25)

Example result:

[File:pifffp_resultfeaturesiisdi.jpg]

Now we have created ourselves a quick way to put features onto map with Lua.