Menu

Show posts

This section allows you to view all posts made by this member. Note that you can only see posts made in areas you currently have access to.

Show posts Menu

Messages - Snarky

#121
It depends a little on what exact effect you want to achieve. For example, do you want them to always be fading in and out, "bouncing" between those states, or would it be more that they are on for a while, then fade out, stay off for a while, then fade in?

And should the fade be at a constant speed, or vary (for example that the lights come on more quickly than they fade out)?

Here is one way to do it if you have 10 fireflies numbered as objects 6-15 (just as an example), and they're meant to stay on and off for some time, but with random variation:

Code: ags
#define FIRST_FIREFLY 6   // Object index of first firefly
#define FIREFLY_COUNT 10  // Number of fireflies

#define FIREFLY_FADEIN_SPEED 10 // Fade in over 0.25 seconds (assuming GameSpeed==40)
#define FIREFLY_FADEOUT_SPEED 3 // Fade out over 0.82 seconds
#define FIREFLY_ON_AVG 120      // On average, stay on for 3 seconds
#define FIREFLY_OFF_AVG 240     // On average, stay off for 6 seconds

bool fadingIn[FIREFLY_COUNT];   // Keep track of whether each firefly is fading in or out

function repeatedly_execute_always()
{
  for(int i=0; i<FIREFLY_COUNT; i++)
  {
    Object* firefly = object[FIRST_FIREFLY+i];
    if(firefly.Transparency == 0) // Firefly on
    {
      if(Random(FIREFLY_ON_AVG-1) == 0) // Wait a random number of cycles
      {
        // Start fading out
        fadingIn[i] = false;
        firefly.Transparency = clamp(firefly.Transparency+FIREFLY_FADEOUT_SPEED, 0, 100);
      }
    }
    else if(firefly.Transparency == 100) // Firefly off
    {
      if(Random(FIREFLY_OFF_AVG-1) == 0)  // Wait a random number of cycles
      {
        // Start fading in
        fadingIn[i] = true;
        firefly.Transparency = clamp(firefly.Transparency-FIREFLY_FADEIN_SPEED, 0, 100);
      }
    }
    else // Continue firefly fading
    {
      if(fadingIn[i])
        firefly.Transparency = clamp(firefly.Transparency-FIREFLY_FADEIN_SPEED, 0, 100);
      else
        firefly.Transparency = clamp(firefly.Transparency+FIREFLY_FADEOUT_SPEED, 0, 100);
    }
  }
}

This code uses a helper function clamp() to ensure that we don't try to set Transparency to a value less than 0 or greater than 100. It's very simple:

Code: ags
int clamp(int value, int min, int max)
{
  if(value<min) return min;
  if(value>max) return max;
  return value;
}
#122
The most well-known site for remote repositories is GitHub, which – as the name suggests – uses git. (For all I know they might support other source control systems as well, but git is the normal one.)

Git can be a little counterintuitive at first, but once you figure out the basics, I think basic use where you're pretty much just using it for backup is very similar between git/svn/cvs.

Keep in mind that there are several different clients you can use for each of them (kind of like there are different web browsers or podcast apps). Some prefer to use the basic command-line tools, but if you're not an expert programmer or experienced with source control, I'd definitely recommend a graphical client. Personally I use SourceTree for git, largely because it is free and available for both Mac and Windows.
#123
I seem to recall that people have experienced similar problems (not just with AGS4) caused by their antivirus, which for some unknown reason hates AGS. You could try disabling it temporarily and see if you can build.
#124
My expectation would be that it should. For example, a Label is a GUIControl, so any method available for GUIControl should be available for Label.
#125
Note that there is a module that makes multiple TextBoxes work the way you would expect, with only a single one having focus:

https://www.adventuregamestudio.co.uk/forums/modules-plugins-tools/module-multitextbox-v1-00-handle-multiple-text-boxes/msg636616479/#msg636616479

It's pretty easy to use. There is also my TextField module that acts as a TextBox replacement and gives you a text cursor (caret) that allows you to edit the text in the middle, use copy-paste, etc. It also has only one TextField having focus at a time.
#126
Quote from: Crimson Wizard on Sat 07/12/2024 16:56:22I am not sure if it matters that it's not used much in adventure games. This may be the case of people not using something because it's not easily available.

Isn't it obvious? AGS is, primarily, an engine for making adventure games. Features that are commonly used in adventure games are well supported (e.g. inventories, dialog), while features that are not commonly used are not reliably supported, or require more work by game developers.

And I'm willing to bet that most adventure games not made in AGS don't use rotation either.

I guess we'll see how many games make extensive use of the rotation capabilities in AGS4.
#127
1) Because rotating is more complicated than moving, since the sprite becomes different.

2) Because rotating sprites isn't usually necessary for adventure games.

In any case, AGS4 adds rotation to objects, characters and GUIs.
#128
Quote from: ThreeOhFour on Fri 06/12/2024 09:43:17Seeing as this is a conversation about best practices, I may as well ask about other ways of doing a single action to multiple elements. Let's say I want to draw l number of lines, using a function call that calculates their various positions based on an incrementing variable. Is there an alternative way of doing this to either writing it all out line by line or calling:

Code: ags
for (int l = 0; l != 17; l++)
{
  DrawLine(l);
}

Does this example have anything to do with the (very cool) lightning arc effect?



A loop is the usual way to apply actions to multiple elements in AGS. Some other languages have a "map" function that allows you to apply another function to all members of a collection directly, but AGS doesn't have anything like that.

But even if you use a loop, there are different ways to do it. The loop could go inside the Drawline function and you call it with an array of line segments, for example.

Another approach could be a recursive function (one that calls itself). You'd have to restructure things a bit so you don't have 17 elements defined in advance, but your Drawline(l) keeps subdividing the problem by calling itself, splitting it up until it reaches the most basic operation. (So, you might call it with the start and end points of the whole arc, and it would check the distance and decide whether to split it up into sections, calling itself to generate each section, until the sections are short enough to just draw a line.) Sometimes this can be an elegant way to solve a problem.

Be careful about recursion, though: it can easily grow out of control on larger problems, with thousands of function calls that can overwhelm the engine.

There's also something called "tail recursion." An example of that would be something like:

Code: ags
managed struct LineSegment
{
  Point startPoint;
  Point endPoint;
  LineSegment* nextSegment;

  import void Draw(DrawingSurface* canvas);
};

void LineSegment::Draw(DrawingSurface* canvas)
{
  // TODO: Draw a line from startPoint to endPoint

  if(nextSegment!=null)
    nextSegment.Draw();
}

With this we can have a linked list of LineSegments, with each segment having a link (a pointer) to the next one. (This structure doesn't really make sense if we want the segments to connect, since the endPoint of one segment needs to be the same as the startPoint of the next, but we'll ignore that for this example.) And you see that the last line of LineSegment.Draw() is another call to LineSegment.Draw(). So if firstSegment is our first segment and we call firstSegment.Draw(), it will in turn call firstSegment.nextSegment.Draw() (the second segment), and so on, until it reaches the end.
#129
Why don't you try it?
#130
Mod note: I split off the scripting discussion and moved it here
#131
Where I can see AI being useful is if you've written some chunk of code that you think ought to work, but doesn't. If you feed that to ChatGPT and ask it to find the problem, it might be able to spot your mistake—even sometimes with AGS code depending on what the problem is.
#132
Quote from: ThreeOhFour on Thu 05/12/2024 10:31:36When I started trying to figure out how I would approach all of this I was debating whether to use custom properties or structs.

While this might look at little clumsy, I will hopefully never have to edit this function again for the duration of developing the game

I'd forgotten that you were using AGS4, actually. In that case I would wrap these custom properties in extender attributes, which cuts out a bunch of the boilerplate elsewhere and solves most of the custom property drawbacks. I've never actually used it, but from the documentation I guess it should look like this?

Code: ags
// Module Header:

import attribute int Energy(this Character);
import attribute int MaxEnergy(this Character);
import attribute int Energy(this Hotspot);
import attribute int MaxEnergy(this Hotspot);
import attribute int X(this Hotspot);
import attribute int Y(this Hotspot);

// Module Script:

// Character.Energy
int get_Energy(this Character)                { return this.GetProperty(ENERGY); }
void set_Energy(this Character, int value)    { this.SetProperty(ENERGY, value); }
// Hotspot.Energy
int get_Energy(this Hotspot)                  { return this.GetProperty(ENERGY); }
void set_Energy(this Hotspot, int value)      { this.SetProperty(ENERGY, value); }
// Character.MaxEnergy
int get_MaxEnergy(this Character)             { return this.GetProperty(MAX_ENERGY); }
void set_MaxEnergy(this Character, int value) { this.SetProperty(MAX_ENERGY, value); }
// Hotspot.MaxEnergy
int get_MaxEnergy(this Hotspot)               { return this.GetProperty(MAX_ENERGY); }
void set_MaxEnergy(this Hotspot, int value)   { this.SetProperty(MAX_ENERGY, value); }
// Hotspot.X
int get_X(this Hotspot)                       { return this.GetProperty(X_COORDINATE); }
void set_X(this Hotspot, int value)           { this.SetProperty(X_COORDINATE, value); }
// Hotspot.Y
int get_Y(this Hotspot)                       { return this.GetProperty(Y_COORDINATE); }
void set_Y(this Hotspot, int value)           { this.SetProperty(Y_COORDINATE, value); }

(The documentation doesn't actually say to use import for the attribute, but that seems most consistent with how other things work.)

Then you can simply use player.Energy and this.Energy (etc.) without needing either local variables or lots of manual calls to SetProperty/GetProperty. Instead of a ChangeProperty helper function and player.ChangeProperty(ENERGY,1) you can just do player.Energy++, for example.
#133
Quote from: Crimson Wizard on Thu 05/12/2024 09:22:24If a line with "min" looks too complicated to read, then it may be replaced with another helper function with speaking name.

IMO the logic is still less transparent, even aside from quibbling over whether the function name is easy to understand (I would suggest "maxPossibleTransfer"), and now you have a function with a bunch of non-obvious parameters that you have to inspect. Instead I'd add a comment to the energyChange declaration to explain the intent of this variable and the logic of the expression:

Code: ags
  // As much energy as we can possibly transfer, either to drain player completely or fill this hotspot completely
  int energyChange = min(playerEnergy, thisMaxEnergy-thisEnergy);

But all this is a lot of work to fix something that I don't really see as a problem in the first place. The loop is, to me, probably the most intuitive and obvious expression of the intent possible. The only drawback is the computer engineer's bias against using loops to do calculations that can be solved analytically (and, it sounds like for @eri0o, that expectation causing confusion).
#134
Quote from: Crimson Wizard on Thu 05/12/2024 08:44:38TBH I'm very much surprised by such view, I have a completely opposite opinion on this.
For me it's not even the question of performance, but a question of clearly declaring a purpose. IMO the second variant has a more clear intent than the second.
For the same reason, it would be easier to write for me personally, I would just do that automatically by reflex.

To me it seems that the intent is "transfer as much energy as possible from player to hotspot; i.e. so that player is empty or hotspot is full." In the first version, that intent is clearly expressed in the loop condition, while in the second it is not apparent to me anywhere in the code: I have to mentally perform the same calculation as the computer in order to determine that that will be the outcome. ("Right, so there are two cases. (1) If thisMaxEnergy-thisEnergy is smaller, then thisEnergy becomes... thisEnergy + thisMaxEnergy - thisEnergy, so just thisMaxEnergy, and playerEnergy becomes... something. (Greater than 0? Let's hope so!) (2) If playerEnergy is smaller, playerEnergy becomes 0, and thisEnergy becomes thisEnergy + playerEnergy, so that's pretty straightforward at least. OK, so what this chunk of code does is...")

The ChangeProperty stuff makes it harder to follow, but even with plain variables I find this:

Code: ags
  while(thisEnergy < thisMaxEnergy && playerEnergy > 0)
  {
    thisEnergy++;
    playerEnergy--;
  }

Easier to understand than this:

Code: ags
  int energyChange = min(thisMaxEnergy-thisEnergy, playerEnergy);
  thisEnergy += energyChange;
  playerEnergy -= energyChange;

Quote from: Crimson Wizard on Thu 05/12/2024 08:44:38But I would suggest to make a function that does "Transfer energy" from object 1 to object 2.

If these are the only three instances in the game where that happens, I'd probably not break it out. Though I might refactor the function logic to reduce it to two.
#135
If we compare:

Code: ags
  while(thisEnergy < thisMaxEnergy && playerEnergy > 0)
  {
    thisEnergy = this.ChangeProperty(ENERGY, 1);
    playerEnergy = player.ChangeProperty(ENERGY, -1);
  }

(Which is how lines 114-118 in the sample Ben showed might look after other suggested improvements.)

With this:

Code: ags
  int energyChange = min(thisMaxEnergy-thisEnergy, playerEnergy);
  thisEnergy = this.ChangeProperty(ENERGY, energyChange);
  playerEnergy = this.ChangeProperty(ENERGY, -energyChange);

(Which is one way to do it without a loop, using a min() helper function.)

... I'm not convinced it's much of an improvement. It might be less work for the computer, but it was harder to write, and I think harder to follow. And the only lines saved are the {}s.
#136
Sweet. It would also be grand if custom properties in AGS4 could use extender attribute APIs instead of needing Get[Text]Property(String key) and Set[Text]Property(String key, [int/String] value). Cf. Ben's latest blog post.
#137
Do not use ChatGPT with AGS!

It's not some ideological thing, either. It just doesn't work. The AGS code ChatGPT proposes is most often nonsense.

In this case, your intuition is correct and ChatGPT is (as usual) wrong:

Code: ags
function room_Leave()
{
  if (object[1].Graphic == 227 && object[2].Graphic == 228 && object[3].Graphic == 229 && object[4].Graphic == 235 && object[5].Graphic == 230 && object[6].Graphic == 231 && object[7].Graphic == 232 && object[8].Graphic == 233 && object[9].Graphic == 234)
  {
    BlueSphereIsTakeable = true;
  }
}

(That if-condition is horrendous, but shouldn't cause any crashes, assuming the objects exist.)

If it still doesn't work, post the actual error messages you get.
#138
If you mean what I think, you should provide one sprite for the "clear" view, one sprite for the "foggy" view, and one sprite for the "brush" template (the shape you want to wipe with, opaque against a transparent background).

Create a "display view" DynamicSprite from the foggy view and display it on screen (for example as an object sprite). When the user clicks on it, figure out the matching sprite coordinates and create a new "brush stamp" DynamicSprite with a copy of the clear view, clipped to match the brush size and the coordinates. Copy the transparency mask from the brush template to the brush stamp, and draw the stamp to the display view (using DrawImage) at the right coordinates. That way, you're drawing a section of the clear view on top of the foggy view, in the shape of the brush.

In order to support gestures, you need to keep track of when the mouse button is down and the mouse moves, and then draw another stamp each time—all the stamps together then form a "brush stroke." (If you want to get really fancy you can also measure the movement distance since the last stamp, and if it is more than some particular value you draw the stamp at intermediate points as well, to ensure that the stroke appears continuous.) Another way to make it fancier is to rotate the brush or use different brushes depending on the direction of the gesture.
#139
The warning is not entirely useless. Of course, it's not really about ensuring you delete DynamicSprites that are meant to persist for as long as the game runs, but ones that are used once but which the user has forgotten to dispose. So if you suddenly start getting this warning unexpectedly, it may point to a bug in recently-added/modified DynamicSprite code. There have been a few times it has helped me spot bugs and failures to dispose sprites after use (e.g. in the SpeechBubble module).

And in practice I find that it's often not too difficult to figure out which sprites aren't being deleted, especially if you otherwise make an effort to clean up things whenever you can (for things that need to persist throughout you can do it when the player uses the in-game quit function, before you call QuitGame—though of course that doesn't apply if they close the game through the OS), so that the warning only appears when something has gone wrong. But if the warning could pinpoint it more precisely (e.g. refer to the line in script that created the Dynamic Sprite), that would certainly make things a bit easier.
#140
No, it's not really necessary: when AGS closes it frees up all the memory anyway. The only reason is so it get rid of the warning. It might be good practice to try to keep your game warning-free, so that if there is a bug elsewhere that creates a bunch of DynamicSprites that are never deleted, you will notice it.
SMF spam blocked by CleanTalk