Remove out of map projectiles! Or how to clean the beach

Ask for help about creating mods and scripts for Grimrock 2 or share your tips, scripts, tools and assets with other modders here. Warning: forum contains spoilers!
User avatar
AndakRainor
Posts: 674
Joined: Thu Nov 20, 2014 5:18 pm

Remove out of map projectiles! Or how to clean the beach

Post by AndakRainor »

Hello !

Here is a script you can add to any level with open borders to destroy projectiles who have left the map :

Code: Select all

old = {}

function check(id)
  local o = findEntity(id)
  if o then
    local X, Y = o.map:getWidth()-1, o.map:getHeight()-1
    local new = {}
    for e in o.map:allEntities() do
      if e.projectile and
       ((e.facing == 0 and e.y == 0) or
        (e.facing == 1 and e.x == X) or
        (e.facing == 2 and e.y == Y) or
        (e.facing == 3 and e.x == 0)) then
        local t = old[e.id] and old[e.id]+1 or 0
        if t>5 then
          e:destroy()
        else
          new[e.id] = t
          --hudPrint(e.name.." at ("..e.x..","..e.y..") for "..(new[e.id]*10).." seconds")
        end
      end
    end
    old = new
    delayedCall(id, 10, "check", id)
  else
    print("Warning : Projectile out of map check end : scriptEntity not found for id = "..id)
  end
end

self.check(self.go.id)
The commented hudPrint line can be uncommented if you want to check what it does in game ! Just throw any spell bolt and look.
I did it because I think it is easier and more clean than putting invisible walls every where.
It will work fine unless for some reason you need a projectile facing and on the border of a map for 1 minute or more.
bongobeat
Posts: 1076
Joined: Thu May 16, 2013 5:58 pm
Location: France

Re: Remove out of map projectiles! Or how to clean the beach

Post by bongobeat »

hello,
thanks for that.

At the moment, I did not see any script like that in the log2 dungeon. The beach levels have not any wall or invisible walls when there is open water.
Is that realy needed to remove the projectile that are out of the map? What's happening then?
My asset pack: viewtopic.php?f=22&t=9320

Log1 mod : Toorum Manor: viewtopic.php?f=14&t=5505
User avatar
THOM
Posts: 1274
Joined: Wed Nov 20, 2013 11:35 pm
Location: Germany - Cologne
Contact:

Re: Remove out of map projectiles! Or how to clean the beach

Post by THOM »

In the original LoG2 Campaign the party is blocked on every beach and can only wade a few steps into the water. At these blockers projectiles and spells are blocked, too.
But if you have a map with elevations +1 and higher, it is possible to cast a spell that overflows the blockers. Then it becomes an infinitive flying object, that will be calculated for the rest of gametime.
THOM formaly known as tschrage
_______________________________________________
My MOD (LoG1): Castle Ringfort Thread
My MOD (LoG2): Journey To Justice Thread | Download
User avatar
AndakRainor
Posts: 674
Joined: Thu Nov 20, 2014 5:18 pm

Re: Remove out of map projectiles! Or how to clean the beach

Post by AndakRainor »

Hello !

I first wanted to see what happens with those projectiles, so I started to write some script to print info about them. I saw that when a projectile leaves the map, it is still considered to be at coordinates inside the map; you never see object.x under zero or above the map width-1. Tracking these infos for a couple minutes I can confirm that those objects don't seem to be ever destroyed and continue to move on with no limit. I don't know if they persist through save and load, but I think they do.

Here on the forum, I saw a few conversations about performance issues, and as we could guess, the graphics rendering is the heaviest part to consider in this domain. Magical projectiles are specially important as they nearly always emit light, may be cast shadows. So I think this script (or some improved version, may be you don't need to check every 10 seconds, may be it could be written for all levels maps at once instead of only for its current one) can be useful... It is not in the main game, because I think it is not so common for a player to throw lots and lots of projectiles out of the starting beach map, until the computer begins to suffer. It really depends on the design of your maps, may be if you have high level zones with open borders like the beach, where a lot of fights take place, involving movement and meteor spells, you should consider something like this script !
bongobeat
Posts: 1076
Joined: Thu May 16, 2013 5:58 pm
Location: France

Re: Remove out of map projectiles! Or how to clean the beach

Post by bongobeat »

Thanks for the precision.

Well I have some area that lead to open water or high area and have but blocker on the edge of the map (on different height too), but I never be sure that I did not forgot one area to "protect". I may use your script, I don't know yet.

Should this script be placed on each level (where is needed) or does it run for all levels? with a timer checking it?
My asset pack: viewtopic.php?f=22&t=9320

Log1 mod : Toorum Manor: viewtopic.php?f=14&t=5505
User avatar
AndakRainor
Posts: 674
Joined: Thu Nov 20, 2014 5:18 pm

Re: Remove out of map projectiles! Or how to clean the beach

Post by AndakRainor »

This script only checks the map it belongs to, so you need one in each level you want to check. Also, the last line "self.check(self.go.id)" makes it auto-start, and then it uses the delayedCall function to repeat every 10 seconds.

All this could be changed, written differently to better match other needs. May be 10 seconds is too high frequency if you put copies in many many levels, I don't know. If you need a reworked script and don't know how to write it, let me know and I will try to create it!
minmay
Posts: 2780
Joined: Mon Sep 23, 2013 2:24 am

Re: Remove out of map projectiles! Or how to clean the beach

Post by minmay »

I'm not a fan of the method in the OP. I think this solution is much more elegant:

Code: Select all

defineObject{
	name = "projectile_cleaner",
	placement = "floor",
	components = {
		{
			class = "Null",
			onInit = function(self)
				for l=1,Dungeon.getMaxLevels() do
					local m = Dungeon.getMap(l)
					spawn("projectile_barrier",l,0,0,0,0):setWorldPosition(m:getWidth()*1.5,0,m:getHeight()*1.5)
				end
				self.go:destroyDelayed()
			end,
		},
	},
	editorIcon = 148,
}
defineObject{
	name = "projectile_barrier",
	placement = "floor",
	components = {
		{
			class = "ProjectileCollider",
			name = "collider_n",
			size = vec(96, 65536, 3),
			debugDraw = true,
			onInit = function(self)
				self:setSize(vec(self.go.map:getWidth()*3,65536,3))
				self:setOffset(vec(0,0,self.go.map:getHeight()*1.5+1.5))
			end,
		},
		{
			class = "ProjectileCollider",
			name = "collider_s",
			size = vec(96, 65536, 3),
			debugDraw = true,
			onInit = function(self)
				self:setSize(vec(self.go.map:getWidth()*3,65536,3))
				self:setOffset(vec(0,0,-self.go.map:getHeight()*1.5-1.5))
			end,
		},
		{
			class = "ProjectileCollider",
			name = "collider_w",
			size = vec(3, 65536, 96),
			debugDraw = true,
			onInit = function(self)
				self:setSize(vec(3,65536,self.go.map:getHeight()*3))
				self:setOffset(vec(-self.go.map:getWidth()*1.5-1.5,0,0))
			end,
		},
		{
			class = "ProjectileCollider",
			name = "collider_e",
			size = vec(3, 65536, 96),
			debugDraw = true,
			onInit = function(self)
				self:setSize(vec(3,65536,self.go.map:getHeight()*3))
				self:setOffset(vec(self.go.map:getWidth()*1.5+1.5,0,0))
			end,
		},
	},
	-- hidden in editor
}
Just place ONE projectile_cleaner object in your dungeon, and it will permanently prevent projectiles from leaving the map on any level of your dungeon. It won't have any undesirable side effects or performance problems.
Grimrock 1 dungeon
Grimrock 2 resources
I no longer answer scripting questions in private messages. Please ask in a forum topic or this Discord server.
User avatar
AndakRainor
Posts: 674
Joined: Thu Nov 20, 2014 5:18 pm

Re: Remove out of map projectiles! Or how to clean the beach

Post by AndakRainor »

Here is a new script you can use in only one script entity and it will check all the maps of your dungeon:

Code: Select all

interval = 1
index = 0
track = {}

function check()
  local levels = Dungeon.getMaxLevels()
  index = index % levels + 1
  local map = Dungeon.getMap(index)
  local old, new = track[index] or {}, {}
  local X, Y, dt = map:getWidth()-1, map:getHeight()-1, interval * levels
  for e in map:allEntities() do
    if e.projectile and
     ((e.facing == 0 and e.y == 0) or
      (e.facing == 1 and e.x == X) or
      (e.facing == 2 and e.y == Y) or
      (e.facing == 3 and e.x == 0)) then
      local t = old[e.id] and old[e.id]+dt or 0
      --local w = e:getWorldPosition()
      --hudPrint(e.name.." at map("..e.x..", "..e.y..") world("..w.x..", "..w.y..", "..w.z..") for "..t.." seconds")
      if t >= 60 then e:destroy() else new[e.id] = t end
    end
  end
  track[index] = new
  delayedCall(self.go.id, interval, "check")
end

check()
Also, if you wish you can change its interval, set to 1 second in this code. The two commented lines can be uncommented to see what it does. Each interval, one different map is checked.

To comment on the side effects, performance and general elegance:

- From my experience, the delayedCall function is very inefficient. If you use it for example to delay hundreds of function calls which will trigger at the same time, or if you use frequently short delays, the result will be very nasty, compared to a timer component. But if you call the same hundreds of functions grouped into only one delayedCall, performance will be good. So with my first script replicated in many maps, in a mod heavily using other delayedCalls, maybe it could have had an impact. The new script resolves this, as it stands alone so the delayedCall is strictly minimal.

- Timer components do not behave at all like the delayedCall function. First, it seems they are more efficient, especially for short intervals like every frames calls. Also, their interval is raised up by A LOT when they belong to an entity not in the current map. On the other hand, the delayedCall function always uses the interval you give to it, so with the first script I gave, all maps would be checked frequently even if non current maps generally change very slowly. The new one also checks all maps with the same frequency, but do so one map at a time. It could be argued that it should perhaps check the current map more often than the others. To have a timer always in the current map, I could have added it to the party, but did not feel like it to keep the code minimal and not too much spread.

- I tested the colliders creation idea, but I think it really is far from ideal. Perhaps it is more efficient, I don't know it depends entirely on the game engine and I don't have the source, so it would need deeper testing. More efficient than my first script running in several maps, yes that seems legit, but more efficient than checking coordinates ? I'm not convinced ! The real problem is it does in no way reproduce the behavior of the script. Colliders on the map borders are like invisible walls so even if they are good from a technical point of view, they are absolutely terrible from a player point of view. Launching a fire ball to have it instantly explode in your face for no visible reason is bad game design. I even tried to place those colliders farther from the borders of the map, and still the result is not acceptable; the collision occurs away from the party, but again for no visible reason and this time with no sound, you then take the damage of the explosion !
minmay
Posts: 2780
Joined: Mon Sep 23, 2013 2:24 am

Re: Remove out of map projectiles! Or how to clean the beach

Post by minmay »

I assume most users would want the functionality of an invisible wall on the border, since, after all, allowing the projectile to leave the map in the first place is surely a bug. If for whatever bizarre reason you do want to allow that, then sure, you're better off deleting the projectile instead of exploding it. In that case I would prefer to include the behaviour in the projectile itself (check world position in an onUpdate hook). Projectile colliders or even checking within the projectile itself are definitely more efficient than iterating over THOUSANDS of elements with allEntities(). Your script is likely to cause noticeable frame delays when it runs.
AndakRainor wrote:Colliders on the map borders are like invisible walls so even if they are good from a technical point of view, they are absolutely terrible from a player point of view. Launching a fire ball to have it instantly explode in your face for no visible reason is bad game design. I even tried to place those colliders farther from the borders of the map, and still the result is not acceptable; the collision occurs away from the party, but again for no visible reason and this time with no sound, you then take the damage of the explosion !
What the hell? Why would you let the player stand on the border of the map in the first place if there isn't a visible wall there already???

I don't know why anyone would want to replicate the behaviour of your script specifically since it deletes the projectiles at inconsistent times (how is the projectile suddenly vanishing less weird than it exploding?)... It also has its own, albeit much smaller, resource leak (you never actually remove the ids from the table). At the very least, if you want to go with the inefficient method of iterating through all entities, you should check world position instead of trying to track times.
Grimrock 1 dungeon
Grimrock 2 resources
I no longer answer scripting questions in private messages. Please ask in a forum topic or this Discord server.
User avatar
AndakRainor
Posts: 674
Joined: Thu Nov 20, 2014 5:18 pm

Re: Remove out of map projectiles! Or how to clean the beach

Post by AndakRainor »

minmay wrote:I assume most users would want the functionality of an invisible wall on the border, since, after all, allowing the projectile to leave the map in the first place is surely a bug. If for whatever bizarre reason you do want to allow that, then sure, you're better off deleting the projectile instead of exploding it.
There is nothing bizarre in allowing a projectile to leave the map if it has open borders, as is a beach map bordered by the sea. It is far more immersive than exploding on invisible walls.
minmay wrote:In that case I would prefer to include the behaviour in the projectile itself (check world position in an onUpdate hook). Projectile colliders or even checking within the projectile itself are definitely more efficient than iterating over THOUSANDS of elements with allEntities(). Your script is likely to cause noticeable frame delays when it runs.
Yes, iterating over allEntities() is a part I also don't like, but still I did not see any frame delays with it. Perhaps with a slow computer or with maps filled with a very high number of entities, but I still doubt it can cause a noticeable effect. If it was the case, this function should be banned from all scripts we could run more than once.
minmay wrote:I don't know why anyone would want to replicate the behaviour of your script specifically since it deletes the projectiles at inconsistent times (how is the projectile suddenly vanishing less weird than it exploding?)...
Well sure the projectiles I have in mind are spell projectiles, facing the direction of their movement, and not so slow that they would still be visible one minute after leaving the map. You should never see anything vanishing unless you have bizarre projectiles in the first place. In this case you can always adapt the parameters of the script. I think the goal is clear; projectiles should disappear when they are far enough to not be seen.

ps:
minmay wrote:What the hell? Why would you let the player stand on the border of the map in the first place if there isn't a visible wall there already???
It is what the main campaign does with the beach, I just re-tested to be sure. Also, the main campaign lets projectiles leave the map in those places.
minmay wrote:It also has its own, albeit much smaller, resource leak (you never actually remove the ids from the table).
I don't remove the content of the table because I remove the table.
minmay wrote:At the very least, if you want to go with the inefficient method of iterating through all entities, you should check world position instead of trying to track times.
That could be a good thing. If all entities is too much to handle (still doubt it), perhaps Map:entitiesAt(number, number) would be more efficient ? I will try to get real numbers to be sure even if I don't see any performance impact, then if needed I will try other methods. But honestly, it did not seem as a big deal when I tried that.
Post Reply