Looping over NPCs
Moderators: marionline, SDHawk
-
- Slime Knight
- Posts: 102
- Joined: Fri Jan 13, 2023 1:13 am
Looping over NPCs
Been reading through the publicly-available scripts, and for the record, what is the particular best way of looping over NPCs to apply behavior, collisions, and the like?
Only using one NPC or one NPC type (like the "Vigilant Guard" in the LOS script) is straightforward but I know at some point I'm going to have to have more than one on a map at a time.
Only using one NPC or one NPC type (like the "Vigilant Guard" in the LOS script) is straightforward but I know at some point I'm going to have to have more than one on a map at a time.
- Bob the Hamster
- Lord of the Slimes
- Posts: 7732
- Joined: Tue Oct 16, 2007 2:34 pm
- Location: Hamster Republic (Ontario Enclave)
- Contact:
Re: Looping over NPCs
If you need to loop over all NPC instances on the map, I recommend "next NPC reference"
https://hamsterrepublic.com/ohrrpgce/do ... creference
https://hamsterrepublic.com/ohrrpgce/do ... creference
-
- Slime Knight
- Posts: 102
- Joined: Fri Jan 13, 2023 1:13 am
Re: Looping over NPCs
Thanks James! I'm using Next NPC Reference - borrowing how Charbile's Hatful Adventure loops over NPCs, and using the LOS script on the wiki, and I've hit a snag. when it gets to the Alter NPC, I get an invalid NPC ID. In the slice debugger it shows ref as -1. the "shadowdude" npc is a global NPC, with an ID of 1. critter:shadowdude is a constant with a value of 1. If I un-comment the "if check line of sight" statement I get an infinite loop. As a test, I replaced the line of sight check with "Delete NPC" and it executes successfully.
Looking at Alter NPC in the dictionary should I be specifying the pool?
Code: Select all
script, critterloop, begin
variable (ref)
ref := Next NPC Reference()
while (ref) do(
if(get NPC Pool(ref) == pool:global) then (
critterbrain(ref)
)
ref := Next NPC Reference()
)
end
# Check whether critters can see the hero
script, critterbrain, ref, begin
variable (FV, RV, npcSl, critterID)
npcSl := get NPC Slice(ref)
critterID := get NPC ID (ref)
switch (critterID) do (
case (critter:shadowdude) do (
#if (check line of sight (NPC X (critter:shadowdude), NPC Y (critter:shadowdude), hero X (me), hero Y (me), NPC direction
(critter:shadowdude)) && stealth < 20) then (
alter NPC (critterID, NPCstat:move type, NPCmovetype:chaseyou)
alter NPC (critterID, NPCstat:move speed, 5)
#)
)
)
end
- Bob the Hamster
- Lord of the Slimes
- Posts: 7732
- Joined: Tue Oct 16, 2007 2:34 pm
- Location: Hamster Republic (Ontario Enclave)
- Contact:
Re: Looping over NPCs
Yes, correct. If you don't tell the alter NPC command which pool, then it assumes you mean the local pool
However there is another problem. When you use alter NPC, you are changing ever copy of that NPC. That probably isn't what you want
The current code means that when any shadowdude passes the line of sight check, they will all start chasing you at once.
Instead, I suggest you create another global NPC like critter:shadowdudeangry
Then instead of alter NPC, use:
Oh! Also, for the "NPC X" and "NPC Y" you should use "ref" instead of "critter:shadowdude". Ref will give you the X Y of the specific instance of the NPC. Using the ID number gives you the X Y of the first copy on the map
However there is another problem. When you use alter NPC, you are changing ever copy of that NPC. That probably isn't what you want
The current code means that when any shadowdude passes the line of sight check, they will all start chasing you at once.
Instead, I suggest you create another global NPC like critter:shadowdudeangry
Then instead of alter NPC, use:
Code: Select all
change NPC id (ref, critter:shadowdudeangry, pool:global)
-
- Slime Knight
- Posts: 102
- Joined: Fri Jan 13, 2023 1:13 am
Re: Looping over NPCs
And that's why I shouldn't program right before bed.
Anyway, a second global NPC for the "attack/angry" state is a great idea, and I've done what you suggested. I also noticed that I wasn't using the "FV" and "RV" variables and fixed that. However, "ref" is still coming up as a -1 when it gets to the line of sight script. There's only one NPC on the map. I'm also including my main loop and init script, in case there's something there.
Anyway, a second global NPC for the "attack/angry" state is a great idea, and I've done what you suggested. I also noticed that I wasn't using the "FV" and "RV" variables and fixed that. However, "ref" is still coming up as a -1 when it gets to the line of sight script. There's only one NPC on the map. I'm also including my main loop and init script, in case there's something there.
Code: Select all
script, main loop, begin
variable (thismap)
thismap := current map
while (thismap == current map) do (
keypresshandler
lucianloop
critterloop
wait (1)
)
end
plotscript, init, begin
stealth := 0
LKstate := state:idle
BaseStealth := 10
set opacity (lookupslice (sl:map layer1), 75)
LKsprite := lookupslice(sl:walkabout sprite)
canshoot := true
#critterInit
main loop
wait
end
...
script, critterloop, begin
variable (ref)
ref := Next NPC Reference()
while (ref) do(
if(get NPC Pool(ref) == pool:global) then (
critterbrain(ref)
)
ref := Next NPC Reference()
)
end
switch (get NPC ID(ref)) do (
case (critter:shadowdude) do (
FV := 6
RV := 3
#delete NPC (ref)
if (check line of sight (NPC X (ref), NPC Y (ref), hero X (me), hero Y (me), NPC direction (ref), FV, RV) && stealth < 20) then (
change npc ID (ref, critter:shadowdudeangry, pool:global)
)
)
)
end
Last edited by Bluefeather42 on Sat May 31, 2025 5:25 pm, edited 1 time in total.
- Bob the Hamster
- Lord of the Slimes
- Posts: 7732
- Joined: Tue Oct 16, 2007 2:34 pm
- Location: Hamster Republic (Ontario Enclave)
- Contact:
Re: Looping over NPCs
Yes, NPC references are always negative numbers, so with one NPC on the map, -1 is exactly what you should expect to see for the ref variable
-
- Slime Knight
- Posts: 102
- Joined: Fri Jan 13, 2023 1:13 am
Re: Looping over NPCs
Gotcha. I was thinking of "Get NPC ID", but I'm still getting an infinite loop when it hits the Line of Sight check.
- Bob the Hamster
- Lord of the Slimes
- Posts: 7732
- Joined: Tue Oct 16, 2007 2:34 pm
- Location: Hamster Republic (Ontario Enclave)
- Contact:
Re: Looping over NPCs
Do you want to post the line of sight script? Maybe there is something inside the script causing the problem?
-
- Slime Knight
- Posts: 102
- Joined: Fri Jan 13, 2023 1:13 am
Re: Looping over NPCs
Here it is. I don't think I changed anything from the one on the wiki besides using FV and RV instead of global variables.
Code: Select all
script, check line of sight, x1, y1, x2, y2, critterdir, FV, RV, begin
variable (distx, disty, hero dir)
distx := x2 -- x1
disty := y2 -- y1
# Special check to prevent divide-by-zero
if (distx == 0 && disty == 0) then (exit returning (true))
# Figure out in which direction from the shadowdude the hero is
# (The result is -1 if they are diagonal)
hero dir := -1
if (disty << 0 && abs(distx) << abs(disty)) then (hero dir := up)
if (disty >> 0 && abs(distx) << abs(disty)) then (hero dir := down)
if (distx << 0 && abs(disty) << abs(distx)) then (hero dir := left)
if (distx >> 0 && abs(disty) << abs(distx)) then (hero dir := right)
# How far can the shadowdude see in this direction?
variable (view dist)
if (critterdir == hero dir) then (
view dist := FV
) else (
view dist := RV
)
# If the hero is too far away, stop. (Pythagoras)
# We add half a tile for smoother circles, then multiply both sides by 4
if (4 * distx^2 + 4 * disty^2 >> (2 * view dist + 1)^2) then (exit returning (false))
# Now check for view obstructions
# x100 and y100 are x and y measured in 100ths of a tile
variable (x100, y100, x100 step, y100 step, num steps, i)
if (abs(distx) >> abs(disty)) then (
# Step one tile in X direction and a fraction of a tile in Y direction at a time
# (The --1 is a trick to prevent asymmetrical fields of view. Difficult to explain)
x100 step := sign(distx) * 100
y100 step := sign(disty) * (abs(100 * disty / distx) -- 1)
num steps := abs(distx)
) else (
# Step one tile in Y direction and a fraction of a tile in X direction at a time
x100 step := sign(distx) * (abs(100 * distx / disty) -- 1)
y100 step := sign(disty) * 100
num steps := abs(disty)
)
# Walk from the hero back to the shadowdude. We check the tile that the hero is standing on,
# but not the one the shadowdude is standing on
x100 := x2 * 100
y100 := y2 * 100
for (i, 1, num steps) do (
# (x100 + 50) / 100 rounds to the nearest tile
if (read zone (zone:LOS obstruct, (x100 + 50) / 100, (y100 + 50) / 100) <> 0) then (exit returning (false))
x100 -= x100 step
y100 -= y100 step
)
# If we get here, the hero is in view
return (true)
end
- Bob the Hamster
- Lord of the Slimes
- Posts: 7732
- Joined: Tue Oct 16, 2007 2:34 pm
- Location: Hamster Republic (Ontario Enclave)
- Contact:
Re: Looping over NPCs
Ah, okay. I guess I don't understand where the current problem is
-
- Slime Knight
- Posts: 102
- Joined: Fri Jan 13, 2023 1:13 am
Re: Looping over NPCs
I'll have to keep fiddling with it, then.
EDIT: Have not delved into the LOS code, but I think the game can work with having one creature on screen at a time. That's my back up plan.
EDIT: Have not delved into the LOS code, but I think the game can work with having one creature on screen at a time. That's my back up plan.
-
- Slime Knight
- Posts: 102
- Joined: Fri Jan 13, 2023 1:13 am
Re: Looping over NPCs
Finally getting to mess with this again, I switched to the latest nightly and it's gotten worse. Critterloop goes into an infinite loop no matter what, regardless if the line of sight script is triggered or not. I'm rolling back to the last stable version and going from there.
Update: That didn't help. Posting the scripts.
Update: That didn't help. Posting the scripts.
Code: Select all
define constant(12345, sli:dataslice)
include,globvar.hss
include,util.hss
# NOTE 2-11-25: There is a HUGE problem somewhere in the projectile scripts that is causing
# issues with the 'main' loop - the only wait command left in the scripts. Originally I had
# to insert a couple of "waits" to get the shooting to actually function. I'm not sure how to
# replace them with timers, and I'm not sure if the projectile scripts aren't completely borked
# when used in a shooter game like this. I'm not sure exactly WHAT it is, but it's only occurring
# when shooting, so the error is somewhere in the projectile scripts.
# Need to look at CHA, shooter game, Don't Eat Soap, anything that has some sort of projectile
# mechanics.
# UPDATE 10:07 AM - The problem was in the "canshoot" timer at the bottom of "arrow eachtick"
# the game now works without error, but now it creates about a billion arrows if CTRL is held
# down.
#5-2-2025: CAN ONLY HAVE A SINGLE WHILE LOOP RUNNING, THIS NEEDS TO CONTROL THE WHOLE ACTION
# GAME SEGMENTS
################################################################################
# Projectiles - From the OHRRPGCE wiki. Modified by removing the "arc" functionality.
# Start a new projectile on an arcing path.
# Speed is the ground speed in pixels per tick.
# x distance and y distance is how far you want to shoot the projectile, relative
# to the initial position (like "move slice by").
# z distance is used when shooting at a target with a different height, e.g. aiming
# at the top of a tree or up a cliff. Positive z distance means shooting upwards
script, fire projectile, slice, speed, x distance, y distance, begin
variable(dist, time)
dist := sqrt(xdistance ^ 2 + ydistance ^ 2)
# Calculate time-in-air, in ticks
time := (dist + speed / 2) / speed
if (time <= 0) then (time := 1)
# Save some other necessary data
prj:x100 := slicex(slice) * 100
prj:y100 := slicey(slice) * 100
prj:v_x100 := xdistance * 100 / time
prj:v_y100 := ydistance * 100 / time
prj:time := time
save projectile data(create data slice(slice))
end
# Update the position of a projectile slice. Needs to be called every tick.
# Returns true if still in flight, or false if the projectile has reached the end.
script, projectile eachtick, slice, begin
variable(dataslice)
dataslice := lookup slice(sli:dataslice, slice)
if (dataslice == false) then (script error($99="Couldn't find projectile data slice"))
load projectile data(dataslice)
if (prj:time <= 0) then (exit returning(false)) # Already finished
prj:time -= 1
prj:x100 += prj:v_x100
prj:y100 += prj:v_y100
save projectile data(dataslice)
# Add 50 to round to nearest pixel
put slice(slice, (prj:x100 + 50) / 100, (prj:y100 + 50) / 100)
return (prj:time > 0)
end
# Set this to the correct map layer, above the hero
define constant(sl:map layer3, projectile layer)
define constant(80, arrow distance)
script, vecX, dir, dist, begin
switch (dir) do (
case (up, down) do (return (0))
case (right) do (return (dist))
case (left) do (return (-1 * dist))
)
end
script, vecY, dir, dist, begin
switch (dir) do (
case (left, right) do (return (0))
case (down) do (return (dist))
case (up) do (return (-1 * dist))
)
end
################################################################################
# Here is one way to run the scripts above: by using a timer.
# Each projectile in the air will need a different timer and a different global variable.
defineconstant(1000, sli:projectile)
defineconstant(15, timer:arrow1)
globalvariable(1, arrow1)
globalvariable(2, canshoot)
script, fire arrow1, begin
set timer(4, 1, 8, set hero picture (0, 0))
arrow1 := load walkabout sprite(1)
setparent(arrow1, lookupslice(projectile layer))
put slice(arrow1, heropixelx, heropixely)
if (hero direction(0) == 0) then (
set sprite frame(arrow1, 1)
) else if (hero direction(0) == 1) then (
set sprite frame(arrow1, 3)
) else if (hero direction(0) == 2) then (
set sprite frame(arrow1, 5)
) else (
set sprite frame(arrow1, 7)
)
variable(x, y)
x := vecX(herodirection, arrow distance) # Find the position 60 pixels ahead of the player
y := vecY(herodirection, arrow distance)
set timer(2, 0, 1, fire projectile(arrow1, 8, x, y)) # 8 pixel/tick, timer added by me
set timer(timer:arrow1, 0, 1, @arrow1 eachtick)
stealth -= 10
end
script, arrow1 eachtick, timer id, begin
if (projectile eachtick(arrow1)) then (
## Still in the air, run timer again
set timer(timer id, 0) # Restarts the timer with a count of 0 (next tick)
) else (
set timer(timer:arrow1, 0, 2, freeslice(arrow1))
canshoot := true
)
end
script, lucianloop, begin
switch (hero is walking) do (
case(true) do (
LKstate := state:walk
)
case(false) do (
LKstate := state:idle
)
)
stealthloop
end
script, main loop, begin
variable (thismap)
thismap := current map
while (thismap == current map) do (
keypresshandler
lucianloop
critterloop
wait (1)
)
end
plotscript, init, begin
stealth := 0
LKstate := state:idle
BaseStealth := 10
set opacity (lookupslice (sl:map layer1), 75)
LKsprite := lookupslice(sl:walkabout sprite)
canshoot := true
#critterInit
main loop
end
#script, critterInit, begin
# Taken from Charbile's Hatful Adventure by KyleKrack, with many thanks
#variable (ref)
#ref := next NPC Reference()
#while (ref) do (
#if (get NPC Pool(ref) == pool:global) then(
#end
script, stealthloop, begin
variable(shadow)
#ambient light is temporary, setting here to make the equation work
ambientlight := 0
############# Set opacity of Lucian's sprite based on lighting zone ###################################################
if (read zone (2, hero X, hero Y)) then (
set opacity (LKsprite, 50)
shadow := 10
) else if (read zone (3, hero X, hero Y)) then (
set opacity (LKsprite, 25)
shadow := 20
) else (
set opacity (LKsprite, 100)
shadow := 0
)
######################## Update stealth #############################################################################
switch (LKstate) do (
case(state:walk) do (
stealth := (BaseStealth + shadow + ambientlight -- 20)
)
case(state:idle) do (
stealth := (BaseStealth + shadow + ambientlight)
)
)
if (stealth <= 0) then (
stealth := 0
)
end
plotscript, keypresshandler, begin
#Note: Wait commands should probably be replaced with timers!
if(key is pressed(key:Ctrl)) then (
if (canshoot == true) then (
canshoot := false
set hero picture (0, 2)
set timer (6, 0, 2, @fire arrow1)
)
)
if(key is pressed(key:p)) then (
show text box (1)
)
end
script, shootarrow, begin
#set timer(4, 1, 2, @fire arrow1)
fire arrow1
set timer(4, 1, 8, set hero picture (0, 0))
end
# "critterloop" and check line of sight slightly modified from the Line of Sight script on the OHR wiki
# and Charbile's Hatful Adventure by KyleKrack, with many thanks to TMC, James Paige, and KyleKrack
script, critterloop, begin
variable (ref)
ref := Next NPC Reference()
while (ref) do(
if(get NPC Pool(ref) == pool:global) then (
critterbrain (ref)
)
)
ref := Next NPC Reference()
end
# Check whether critters can see the hero
script, critterbrain, ref, begin
variable (npcSl)
npcSl := get NPC Slice(ref)
switch (get NPC ID(ref)) do (
case (critter:shadowdude) do (
delete npc(ref)
)
)
end
# Check whether tile #2 is in view from tile #1, when facing a certain direction.
# Returns true or false.
script, check line of sight, x1, y1, x2, y2, critterdir, FV, RV, begin
variable (distx, disty, hero dir)
distx := x2 -- x1
disty := y2 -- y1
# Special check to prevent divide-by-zero
if (distx == 0 && disty == 0) then (exit returning (true))
# Figure out in which direction from the shadowdude the hero is
# (The result is -1 if they are diagonal)
hero dir := -1
if (disty << 0 && abs(distx) << abs(disty)) then (hero dir := up)
if (disty >> 0 && abs(distx) << abs(disty)) then (hero dir := down)
if (distx << 0 && abs(disty) << abs(distx)) then (hero dir := left)
if (distx >> 0 && abs(disty) << abs(distx)) then (hero dir := right)
# How far can the shadowdude see in this direction?
variable (view dist)
if (critterdir == hero dir) then (
view dist := FV
) else (
view dist := RV
)
# If the hero is too far away, stop. (Pythagoras)
# We add half a tile for smoother circles, then multiply both sides by 4
if (4 * distx^2 + 4 * disty^2 >> (2 * view dist + 1)^2) then (exit returning (false))
# Now check for view obstructions
# x100 and y100 are x and y measured in 100ths of a tile
variable (x100, y100, x100 step, y100 step, num steps, i)
if (abs(distx) >> abs(disty)) then (
# Step one tile in X direction and a fraction of a tile in Y direction at a time
# (The --1 is a trick to prevent asymmetrical fields of view. Difficult to explain)
x100 step := sign(distx) * 100
y100 step := sign(disty) * (abs(100 * disty / distx) -- 1)
num steps := abs(distx)
) else (
# Step one tile in Y direction and a fraction of a tile in X direction at a time
x100 step := sign(distx) * (abs(100 * distx / disty) -- 1)
y100 step := sign(disty) * 100
num steps := abs(disty)
)
# Walk from the hero back to the shadowdude. We check the tile that the hero is standing on,
# but not the one the shadowdude is standing on
x100 := x2 * 100
y100 := y2 * 100
for (i, 1, num steps) do (
# (x100 + 50) / 100 rounds to the nearest tile
if (read zone (zone:LOS obstruct, (x100 + 50) / 100, (y100 + 50) / 100) <> 0) then (exit returning (false))
x100 -= x100 step
y100 -= y100 step
)
# If we get here, the hero is in view
return (true)
end
- Bob the Hamster
- Lord of the Slimes
- Posts: 7732
- Joined: Tue Oct 16, 2007 2:34 pm
- Location: Hamster Republic (Ontario Enclave)
- Contact:
Re: Looping over NPCs
In critterloop, this line here:
Is outside of the while loop. It needs to be moved inside
Code: Select all
ref := Next NPC Reference()
- Bob the Hamster
- Lord of the Slimes
- Posts: 7732
- Joined: Tue Oct 16, 2007 2:34 pm
- Location: Hamster Republic (Ontario Enclave)
- Contact:
Re: Looping over NPCs
Oh, and you need to pass the previous ref as an argument
Code: Select all
ref := Next NPC Reference(ref)
- Bob the Hamster
- Lord of the Slimes
- Posts: 7732
- Joined: Tue Oct 16, 2007 2:34 pm
- Location: Hamster Republic (Ontario Enclave)
- Contact:
Re: Looping over NPCs
Oh, and since you might delete the NPC inside critterbrain you need to do this:
Code: Select all
script, critterloop, begin
variable (ref, next ref)
ref := Next NPC Reference()
while (ref) do(
next ref := Next NPC Reference(ref)
if(get NPC Pool(ref) == pool:global) then (
critterbrain (ref)
)
ref := next ref
)
end