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

Topics - LostTrainDude

#1
In my space trading game prototype I have also NPCs who run errands in such way:

  • A new random mission is assigned to them, from the system they are currently on, to a random system (different from the one they are on)
  • They are "flagged" as being on mission
  • They travel from ORIGIN to TARGET
  • Upon arrival on TARGET they are "flagged" as being NOT on mission
  • A new random mission is assigned to them
  • Go to point 2

The problem I have is that this "routine" does not seem to happen "endlessly" as it should. It randomly lasts an average 3 to - maybe - 5 cycles, then the NPC will get a new mission, with new coords, and just sit there, although they are flagged as being "on mission".

I have double checked and the coords are always within the bounds and "real", so I'm not sure what I'm getting wrong. Maybe someone here can provide some feedback?

Here comes the code in the order in which it is compiled:
Code: ags
// Movement uses a pretty basic algorithm.
// Manhattan distance to calculate the distance between the NPC and its target

function Person::Move(int toX, int toY)
{
   int x1 = this.X;
   int y1 = this.Y;
   
   int x2 = toX;
   int y2 = toY;
   
   float tx = IntToFloat(x2 - x1);
   float ty = IntToFloat(y2 - y1);
   
   int dist = GetDistance(this.X, this.Y, x2, y2);
   
   if (dist > 0)
   {
      tx = tx/IntToFloat(dist);
      ty = ty/IntToFloat(dist);
      this.X += FloatToInt(Maths.Ceil(tx));
      this.Y += FloatToInt(Maths.Ceil(ty));
   }
}

Code: ags
/// Move People who are flagged as being on mission towards their destination

function MovePerson(int who)
{
   // The ID of the mission an NPC is on (always 0 in this test)
   int mission_id = People[who].MissionID;
   
   // The ID of the target planet
   int target_planet = Missions[mission_id].TargetPlanet;
   
   // The current X/Y coords of the NPC
   int pX = People[who].X;
   int pY = People[who].Y;
   
   // The X/Y coords of the target planet
   int toX = planetarySystem[target_planet].X;
   int toY = planetarySystem[target_planet].Y;

   // If the NPC is on the target planet
   if (pX == toX && pY == toY)
   {
      // Then is not on mission anymore
      People[who].IsOnMission = false;
      
      // Former target planet becomes new starting position
      People[who].Where = Missions[mission_id].TargetPlanet;
   }     
   else
   {
      // Move towards the target planet
      People[who].Move(toX, toY);
   }
}

Code: ags
// This is the main function who controls the routine

function NextTurn(int turns)
{
   for (int t = 0; t < turns; t++)
   {
      // If NPC (always 0 in this test) is flagged as being on mission
      if (People[0].IsOnMission)
      {
         // Then move NPC (always 0 in this test)
         MovePerson(0);
      }
      else
      {
         // Check that this code is actually being executed
         Display("New Mission!");
         int person = 0; // Always 0 in this test
        
         // Select a random type of mission
         int type = Random(MISSION_TYPES-1);
         
         // Select the starting planet to be the same as the one the NPC is sitting on
         int startPlanet = People[person].Where;
         
         // Select a random target planet
         int targetPlanet = Random(TOTAL_SYSTEMS-1);
         
         // Repeat if start and target planets are the same
         if ((targetPlanet == startPlanet))
         {
            do
            {
               targetPlanet = Random(TOTAL_SYSTEMS-1);
            } while ((targetPlanet == startPlanet));
         }
         
         Missions[0].AssignedTo = 0; // The ID of the NPC to which this mission is assigned
         Missions[0].ID = 0; // The ID of the mission
         Missions[0].OriginPlanet = startPlanet;
         Missions[0].TargetPlanet = targetPlanet;
         Missions[0].Type = type;
         
         People[0].MissionID = 0; // The same as Mission[0].ID
         People[0].IsOnMission = true;
      }
      // Draw the pixels on the screen!
      map.DrawPixelPeople();
   }
}


Code: ags
// This is the debug function I'm using to test the whole thing

function on_key_press(eKeyCode keycode) 
{
   if (keycode == eKey1)
   {
      for (int i = 0; i < 1500; i++)
      {
         NextTurn(1);
         Wait(1); // I want to see it moving on the screen!
      }
   }
}


Thanks in advance and I hope I have made everything clear!

EDIT:

The issue is apparently solved by changing Math.Ceil() to Math.Round() in the Person::Move() function.
vga256 suggested me to go deeper with the debug and I just did that. I'm not sure why the calculation caused the problem, but now it seems to be working as intended!
#2
I have been struggling for about a week to convert this pseudocode (and its C++ implementation) in AGS.

After quite some time I was able to shrink down the script to just integers and bools, which made everything more easy to manage and edit. I made some progress but I'm not there yet.

I am using a grid of nodes, 8px distant from each other. Each Node has an x and a y that can be used to calculate their node number within a global array of Nodes.
Code: ags
#define NCOLS 8
#define NROWS 8

int GetNodeAtRowCol(int row, int col)
{
   return row*NCOLS+col;
}

int GetNodeAtYX(int y, int x)
{
   return GetNodeAtRowCol((y - 8) / 8 , (x - 8) / 8);
}

I started with a 24x24 grid, but 576 nodes were too much for the search algorithm, who started running those pesky loops running "150001 times" and more. So, for testing purposes, I scaled down, up to 8x8, so 64 nodes. Things went better but as soon as I started moving from a less complex algorithm, such as the Breadth First search, towards the A* I started getting those again.

I tried my best to make the code as simple as possible, splitting much of it into separate functions, also having to write my own versions of push(), pop(), insert(), etc. Still, it returns such "infinite loop" errors: 90% of the time it won't work from the very start, 10% of the time it would crash after a few moments.

Of course, I do not exclude that I may have made some mess.
Can you make sense of it? I tried my best to comment it and I hope it is understandable.

Most of all I'd like to know if it's me just pushing this too far or if there is something wrong with what I have coded.

I have marked with an asterisk (*) the lines that trigger the error, but I suppose it was quite obvious.

Code: ags
// BigBang.ash
struct Node
{
   int x, y;
   int neighbours[8];  
   bool isFree;
   int cost;
}

(cost at the moment is either 1 or 2, depending on whether there is a planet on there or not)

Code: ags
// Pathfinding.asc
int frontier[TOTAL_NODES];
int frontier_priority[TOTAL_NODES];
int came_from[TOTAL_NODES];
int cost_so_far[TOTAL_NODES];

int heuristic(int node_a, int node_b)
{   
   float n1x = IntToFloat(g_Nodes[node_a].x);
   float n1y = IntToFloat(g_Nodes[node_a].y);
   float n2x = IntToFloat(g_Nodes[node_b].x);
   float n2y = IntToFloat(g_Nodes[node_b].y);
   
   return FloatToInt(Maths.Ceil(Maths.Abs(n1x - n2x) + Maths.Abs(n1y - n2y)));
}

// Initialize all of the arrays for the search function
function InitializeArrays()
{
   for (int i = 0; i < TOTAL_NODES; i++)
   {
      frontier[i] = -1;
      came_from[i] = -1;
      cost_so_far[i] = -1;
      frontier_priority[i] = -1;
   }   
}

// Check if there is at least one valid element in the array
bool IsFrontierEmpty()
{
   for (int i = 0; i < TOTAL_NODES; i++)
   {
      if (frontier[i] != -1)
      {
         return false;
      }
   }
   return true;
}

// "Remove" the first valid element in the array and return its value
int Frontier_GetFirstNode()
{
   int i, ret;
   for (i = 0; i < TOTAL_NODES; i++)
   {
      // if something different than -1 is encountered
      if (frontier[i] != -1)
      {
         // break the loop
         break;
      }
   }
   // store the value at the position
   ret = frontier[i];
   
   // "nullify" the element
   frontier[i] = -1;
   frontier_priority[i] = -1;
   
   // return the stored value;
   return ret;
}

// Insert 
function Frontier_Insert(int node, int priority)
{
   int cur;
   
   // From the back of the array, copy each element to the following index, until desired position is reached
   for (cur = TOTAL_NODES-2; cur >= 0; cur--)
   {
      frontier[cur+1] = frontier[cur];
      frontier_priority[cur+1] = frontier_priority[cur];
   }
      
   // Fill the element at the desired position with the desired node's values
   frontier[0] = node;
   frontier_priority[0] = priority;
} 

// Insert an element at the front of the array
function CameFrom_Insert(int node)
{
   int cur;
   
   // Copy each element to the slot next to them, until desired position is reached
   for (cur = TOTAL_NODES-2; cur >= 0; cur--)
   {
      came_from[cur+1] = came_from[cur];
   }
      
   // Fill the element at the desired position with the desired node's values
   came_from[0] = node;   
}

// Return the first valid element from the back of the array
int CostSoFar_GetLastNode()
{
   for (int j = TOTAL_NODES-1; j > 0; j--)
   {
      if (cost_so_far[j] != -1)
      {
         return cost_so_far[j];
      }
   }
}

// Check if the given node is already included in the array
bool CostSoFar_CheckNode(int node)
{
   for (int i = 0; i < TOTAL_NODES; i++)
   {
      if (cost_so_far[i] == node)
      {
         return true;
      }
   }
   return false;
}

// Foresee the cost of the next step in the search algorithm
int CostSoFar_NextStepCost(int node)
{
   for (int i = 0; i < TOTAL_NODES; i++)
   {
      if (g_Nodes[i] == g_Nodes[node])
      {
         return g_Nodes[i].cost;
      }
   }
}

// Insert an element after the last valid one in the array
function CostSoFar_InsertToBack(int cost)
{
   for (int i = TOTAL_NODES-1; i > 0; i--)
   {
      if (cost_so_far[i] != -1)
      {
         cost_so_far[i+1] = cost;
      }
   }
}

String a_star_search(int nStart, int nGoal)
{
   // Initialize all arrays elements to -1
   InitializeArrays();
   
   // INIT start_location
   int start_location = nStart;
   int start_location_nbs[8];
   for (int i = 0; i < 8; i++) start_location_nbs[i] = g_Nodes[nStart].neighbours[i];
   
   // Add start_location to frontier
   frontier[0] = nStart;
   frontier_priority[0] = 0;
   
   // Add start_location to frontier
   came_from[0] = nStart;
   
   // Add current cost of 0;
   cost_so_far[0] = 0;
   
   // As long as there is something different than -1 in frontier[]
   while (!IsFrontierEmpty())
   {
      // Get the first element in frontier[]
      int f_node = Frontier_GetFirstNode();
      
      // INIT current_location
      int current_location = f_node;
      int current_location_nbs[8]; // neighbours
      for (int i = 0; i < 8; i++)
      {
         current_location_nbs[i] = g_Nodes[f_node].neighbours[i];
      }
      
      // Stop searching once arrived to the destination
      if (f_node == nGoal)
      {
         break;
      }
     
      
      // Scan the neighbours to see if they have been visited
      for (int i = 0; i < 8; i++)
      {
         // Store the node
         int neighbour_node = current_location_nbs[i];
         
         // If the node is within the bounds
         if (neighbour_node != -1)
         {
            // Calculate: last cost + the cost from the last location to the current one
            int new_cost = CostSoFar_GetLastNode() + (CostSoFar_GetLastNode()+g_Nodes[neighbour_node].cost);
            
            // Test the next iteration in advance to check whether it's valid or not
            if (i+1 < 8 && current_location_nbs[i+1] != -1)
            {
               // If it's valid calculate the cost of the next location
               int next_cost = CostSoFar_NextStepCost(current_location_nbs[i+1]);
               
               // If we didn't visit this node yet OR if it is less expensive
               if (!CostSoFar_CheckNode(current_location_nbs[i+1]) || new_cost < next_cost)
               {
                  // Add the cost to the cost_so_far array
                  CostSoFar_InsertToBack(new_cost);
                  
                  // Establish the priority
                  int priority = new_cost + heuristic(nGoal, current_location_nbs[i+1]);
                  
                  // Insert the next neighbour at the top of the frontier array;
                  Frontier_Insert(current_location_nbs[i+1], new_cost); // *
                  
                  // Insert the next neighbour at the top of the came_from array;
                  CameFrom_Insert(current_location_nbs[i+1]); // *
               }
            }
         }
      }
   }
   
   
   // Store the path into a string, separating values with comma
   String s = "";
   for (int i = TOTAL_NODES-1; i > 0; i--)
   {
      if (came_from[i] != -1)
      {
         String ss = String.Format("%d,", came_from[i]);
         s = s.Append(ss);
      }
   }
   return s;
}

export a_star_search;


It is also worth noticing that the frontier_priority array, theoretically to be used "side by side" with the frontier one, is still not used in any kind of sorting (which at this stage I'm not really sure how to tackle).

Thanks a lot in advance to anyone who spends some time on this. Also thanks again to Crimson Wizard, Gurok* and vga256 who have helped a lot already.
I wonder if this can be turned into a module, once (and if) it will be figured out. :)

*Gurok: if you're reading this, the last code you shared with me helped me a great bunch, although I was not able to figure out the circular queue, yet :(
#3
As I am trying to work my way around missing equivalents of functions such as push(), pop() and empty() for arrays, I created my own Pop() function.

The idea is that: iterating through an array of already initalized managed structs, as soon as the loop finds an element that has its valid variable value set to true, it should:
  • switch it to false;
  • break out of the loop and exit the function.
Problem is: it exits, but for some reason doesn't stop at the first element it finds.
I know it exits because I tried creating breakpoints in the loop and debugging it step by step and apparently the loop breaks\returns when it should.
Still, the results are still not what I think I coded them to be (unless I made some silly mistake, and that could be possible).

This is the code I'm currently using:

Code: ags
managed struct PathNode
{
   bool valid;
};

PathNode* frontier[TOTAL_NODES];

PathNode* current;

function Pop(String array)
{
   for (int n = 0; n < TOTAL_NODES; n++)
   {
      if (array == "frontier")
      {
         if (frontier[n].valid)
         {
            frontier[n].valid = false;
            return; // also tried using break
         }
      }
   }
}

function Push(String array, PathNode* to_add)
{
   for (int n = TOTAL_NODES-1; n >= 0; n--)
   {
      if (array == "frontier")
      {
         if (frontier[n].valid)
         {
            frontier[n+1].valid = true;
            frontier[n+1] = to_add;
         }
         else if (n == 0)
         {
            frontier[n].valid = true;
            frontier[n] = to_add;
         }
      }
   }
}

function game_start()
{
   current = new PathNode;
   current.valid = true;
   
   for (int n = 0; n < TOTAL_NODES; n++)
   {
      frontier[n] = new PathNode;
      frontier[n].valid = false;
   }
   
   if (Empty("frontier")) Display("Frontier is EMPTY");
   else Display("Frontier is NOT EMPTY");
   
   Push("frontier", current);
   Push("frontier", current);
   Push("frontier", current);
   
   for (int n = 0; n < 5; n++)
   {
      Display("frontier\[%d]: %d", n, frontier[n].valid);
   }
   
   // Outputs:
   // Frontier[0]: 1
   // Frontier[1]: 1
   // Frontier[2]: 1
   // Frontier[3]: 0
   // Frontier[4]: 0
   
   
   Pop("frontier");
   
   for (int n = 0; n < 5; n++)
   {
      Display("frontier\[%d]: %d", n, frontier[n].valid);
   }
   
   // Outputs:
   // Frontier[0]: 0 
   // Frontier[1]: 0 -- SHOULD BE 1
   // Frontier[2]: 0 -- SHOULD BE 1
   // Frontier[3]: 0 
   // Frontier[4]: 0
}


I also tried writing the loop like this, but it still produces the same results:

Code: ags

function Pop(String array)
{
   bool found = false;
   for (int n = 0; n < TOTAL_NODES; n++)
   {
      if (!found)
      {
         if (array == "frontier")
         {
            if (frontier[n].valid)
            {
               frontier[n].valid = false;
               found = true;
            }
         }
      }
   }
}


I even tried not doing everything altogether in the game_start()

Code: ags
function on_key_press(eKeyCode keycode) 
{ 
   if (keycode == eKeyU)
   {
      Pop("frontier");
      for (int n = 0; n < 5; n++)
      {
         Display("frontier\[%d]: %d", n, frontier[n].valid);
      }
   }
}


Thanks in advance to anyone who can shed some light.
#4
Hi all!

I am tinkering with this idea of creating some sort of space trading sim in AGS and, after managing to scatter some "VIPs" in the universe, I wondered if I could have them run their own errands\missions.
These errands\missions, along with other features, are all going to have a start date and a due date. This led me to implement an internal clock and also a "calendar", in order to make date differences calculations.

I treated dates as managed structs as defined here:
Code: ags
// Time.ash

managed struct Date
{
   int Hour;
   int Minute;
   int Second;
   
   int Year;
   int Month;
   int Day;
   
   import bool IsLeap();
   import function Add(int days);
};

import int DateToRaw(int y, int m, int d); // converts years, months, days to days
import Date* RawToDate(int rawDate); // converts days to years, months, days

import function Clock(int speed); // speed = 40*x, where 40 is 1 second in game loops

import Date* StartDate; // set to 01-01-3200
import Date* CurrentDate; // updated by the clock


Code: ags
// Time.asc

Date* StartDate;
Date* CurrentDate;

int tick; // Game loops

// I know it is kind of a duplicate but I was testing different solutions
bool leapyear(int year)
{
   if (year %   4 != 0) return false;
   if (year % 100 != 0) return true;
   if (year % 400 != 0) return false;
   return true;
   
   //Alternative one-liner
   /* return year%4 == 0 && (year%100 != 0 || year%400 == 0); */
}

// I know it is kind of a duplicate but I was testing different solutions
bool Date::IsLeap()
{
   return leapyear(this.Year);
}

// This increases Date of x number of days, and keeps track of months and years
// It is not used in this example here but it works perfectly fine
function Date::Add(int days)
{ 
   for (int i = 0; i < days; i++)
   {
      this.Day++;
      
      if (this.Day > 28 && this.Month == 2)
      {
         if (!this.IsLeap())
         {
            this.Day = 1;
            this.Month++;
         }
         else
         {
            if (this.Day > 29)
            {
               this.Day = 1;
               this.Month++;
            }
         }
      }
      else if (this.Day > 30 && (this.Month == 11 || this.Month == 4 || this.Month == 6 || this.Month == 9))
      {
         this.Day = 1;
         this.Month++;
      }
      else if (this.Day > 31 && (this.Month == 1 || this.Month == 3 || this.Month == 5 || this.Month == 7 || this.Month == 8 || this.Month == 10 || this.Month == 12))
      {
         this.Day = 1;
         this.Month++;
         if (this.Month == 13)
         {
            this.Month = 1;
            this.Year++;
         }
      }
   }
}

// Convert YYYY-MM-DD to days
// Found on stackoverflow, here https://stackoverflow.com/a/14224238
int DateToRaw(int y, int m, int d)
{
   /* Rata Die day one is 0001-01-01 */
   if (m < 3)
   {
      y--;
      m += 12;
   }
   return 365*y + y/4 - y/100 + y/400 + (153*m - 457)/5 + d - 306;
}

// Convert days to YYYY-MM-DD
// includes code provided by vga256 and most of the code from the Date.Add() function
Date* RawToDate(int rawDate)
{
   // Initialize a new Date object
   Date* d = new Date;
   d.Year = 1;
   d.Month = 1;
   d.Day = 1;
   d.Second = 0;
   d.Minute = 0;
   d.Hour = 0;
   
   int rawdays = rawDate;
   int howmanyleapyears;
   
   // To make it start at 3200 subtract 1
   d.Year = rawdays / 365;
   d.Year--; // *
   
   // How many leap years from year 0001 to rawDate?
   //
   // PLEASE NOTE: I do this to keep the number of iterations low
   // in order to prevent the loop from hanging
   for (int i = 1; i < d.Year; i++)
   {
      if (leapyear(i)) howmanyleapyears++;
   }
   
   // If the current year is a leap year then calculate the
   // remainder out of 366 days
   if (d.IsLeap()) rawdays = (rawdays - howmanyleapyears) % 366;
   
   // Otherwise calculate the remainder out of 365 days
   else rawdays = (rawdays - howmanyleapyears) % 365;

   // Same as the one in the Clock() function
   for (int i = 0; i < rawdays; i++)
   {
      d.Day++;
      
      if (d.Day > 28 && d.Month == 2)
      {
         if (!d.IsLeap())
         {
            d.Day = 1;
            d.Month++;
         }
         else 
         {
            if (d.Day > 29)
            {
               d.Day = 1;
               d.Month++;
            }
         }
      }
      else if (d.Day > 30 && (d.Month == 11 || d.Month == 4 || d.Month == 6 || d.Month == 9))
      {
         d.Day = 1;
         d.Month++;
      }
      else if (d.Day > 31 && (d.Month == 1 || d.Month == 3 || d.Month == 5 || d.Month == 7 || d.Month == 8 || d.Month == 10 || d.Month == 12))
      {
         d.Day = 1;
         d.Month++;
         if (d.Month == 13)
         {
            d.Month = 1;
            d.Year++;
         }
      }
   }
   return d;
}

// Tick tock!
// The clock works perfectly fine
function Clock(int speed)
{
   tick -= 1*speed;
   if (tick <= 0)
   {
      CurrentDate.Second++;
      if (CurrentDate.Second == 60)
      {
         CurrentDate.Second = 0;
         CurrentDate.Minute++;
         if (CurrentDate.Minute == 60)
         {
            CurrentDate.Minute = 0;
            CurrentDate.Hour++;
            if (CurrentDate.Hour == 24)
            {
               CurrentDate.Hour = 0;
               CurrentDate.Day++;
               if (CurrentDate.Day > 28 && CurrentDate.Month == 2)
               {
                  if (!CurrentDate.IsLeap())
                  {
                     CurrentDate.Day = 1;
                     CurrentDate.Month++;
                  }
                  else 
                  {
                     if (CurrentDate.Day > 29)
                     {
                        CurrentDate.Day = 1;
                        CurrentDate.Month++;
                     }
                  }
               }
               if (CurrentDate.Day > 30 && (CurrentDate.Month == 11 || CurrentDate.Month == 4 || CurrentDate.Month == 6 || CurrentDate.Month == 9))
               {
                  CurrentDate.Day = 1;
                  CurrentDate.Month++;
               }
               else if (CurrentDate.Day > 31 && (CurrentDate.Month == 1 || CurrentDate.Month == 3 || CurrentDate.Month == 5 || CurrentDate.Month == 7 || CurrentDate.Month == 8 || CurrentDate.Month == 10 || CurrentDate.Month == 12))
               {
                  CurrentDate.Day = 1;
                  CurrentDate.Month++;
                  if (CurrentDate.Month == 13)
                  {
                     CurrentDate.Month = 1;
                     CurrentDate.Year++;
                  }
               }                      
            }
         }
      }
      tick = 40;
   }  
}

function repeatedly_execute()
{
   // Runs and displays the clock on screen in a certain room
   if (player.Room == 2)
   {
      Clock(1);
      String s = String.Format("%.2d-%.2d-%.2d \\ %.2d:%.2d:%.2d", CurrentDate.Day, CurrentDate.Month, CurrentDate.Year, CurrentDate.Hour, CurrentDate.Minute, CurrentDate.Second);
      lblCurrentTime.Text = s;
   }
}

function game_start()
{
   StartDate = new Date;
   CurrentDate = new Date;
   
   tick = 40;
   
   StartDate.Year = 3200;
   CurrentDate.Year = StartDate.Year;
   
   StartDate.Month = 1;
   CurrentDate.Month = StartDate.Month;
   
   StartDate.Day = 31;
   CurrentDate.Day = StartDate.Day;
   
   StartDate.Second = 50;
   CurrentDate.Second = StartDate.Second;
   
   StartDate.Minute = 59;
   CurrentDate.Minute = StartDate.Minute;
   
   StartDate.Hour = 23;
   CurrentDate.Hour = StartDate.Hour;
   String s = String.Format("%.2d-%.2d-%.2d \\ %.2d:%.2d:%.2d", CurrentDate.Day, CurrentDate.Month, CurrentDate.Year, CurrentDate.Hour, CurrentDate.Minute, CurrentDate.Second);
   lblCurrentTime.Text = s;   
}

export CurrentDate;
export StartDate;


Last but not least, I have bound to a key this block of code, which calls the above function and displays the returned string:
Code: ags
String s;
int a = DateToRaw(CurrentDate.Year, CurrentDate.Month, CurrentDate.Day); // = 1168441 
Date* b;
b = RawToDate(a);
s = String.Format("%.2d-%.2d-%.2d", b.Day, b.Month, b.Year);
return s;


Now, given all of this, the problem I'm having (actually me and vga256 are having, since he is helping me a great deal to crack this) is that, when I call this function:

1) If I don't subtract 1 from d.Year (check line 99 of the Time.asc): Months and Days show correctly, but the year is off.


2) If I subtract 1 from d.Year: the year is fine but Months and Days are quite off.


Does anyone have an idea why this is happening?

To give you an idea of what my ideal outcome would be, at the end of the day, here is a mockup:


Thanks a million in advance!
#5
THE FICKLE HANDS OF FATE
A short single-click point and click adventure game



[imgzoom]http://www.adventuregamestudio.co.uk/images/games/2159_1.png[/imgzoom][imgzoom]http://www.adventuregamestudio.co.uk/images/games/2159_2.png[/imgzoom]

Download from the AGS Games Database

Fate is finalizing her last recording in the steeple of an abandoned church in a village she is about to leave. Then, off to another city and another recording: her search is not over yet.

----

With this one I experimented a little by using a "single-click" gameplay.
So rather than having a standard BASS mechanic (Left Click to Interact, Right Click to Examine) I moved what would usually fall under the "Examine" verb right on screen.

If you hover on any hotspot or object a popup will appear displaying what the character thinks when looks at that specific hotspot or object.
It's some sort of visual stream of consciousness, if you will.

One thing I was not able to implement properly within the time limit was the "stream of consciousness" for the "use X with Y" interactions.
I tested it and it worked, but since the game changes some of the hotspots' descriptions at runtime, I was starting to get a little lost, therefore I preferred to keep it out for this release.

An example in a little spoiler ahead:
Spoiler
For instance, if you tried using the pillow on Sam, the popup would have displayed something like: "I would love to hit him with the pillow, but maybe it's better not to"
[close]
I'm open to feedback about such design and also, if any of you has suggestions, throw them at me :D
#6
I thought I published this already on these forums, but turns out I didn't!



Far in the North, where nighttime can last for months, a researcher is left alone with his boredom and an important research close to completion. Then, a power shortage gets in the middle.

The game is available for PC, Mac and Linux!

Download for free (or pay-what-you-want) on itch.io

OR

Download on GameJolt
Download on IndieExpo
Download on IndieGameStand


Game Features

  • Short, yet authentic point and click fun!
  • Chunky pixel-art, just because!
  • Debatable science!
  • Pseudo-random soundtrack!
  • Some John Conway's Game of Life inspired graphics!

Additional Info

I originally developed this game for December 2014 MAGS, then ported it a year later to Unity and re-released it with my brother helping me on optimizing the original graphics and creating some additional art.

The original is still available, of course, so you can compare versions if you want :)

Trivia

When Thimbleweed Park was looking for books to fill its huge library shelves, I wrote a short novelization of the game. Seeing it in the game made my little nerdy heart beat with joy :-D



#7
As per subject, I'm trying to make a character reach a very specific destination, before performing an animation.
This is the line I've been using:
Code: ags
player.WalkStraight(object[0].X+(Game.SpriteWidth[object[0].Graphic]/2), player.y, eNoBlock);


The result is that most of the time the character gets a few pixels off the target. For instance: while that specific x is 733, the Character reaches 732 or 731.

I tried using both Character.Walk() and Character.WalkStraight(), also I originally had just object[0].X for the x parameter of the function. The results are the same.
Is this something strictly related to the way those function work? Is it because of the walking animation, perhaps?

It is probably worth mentioning that the object[0] is placed everytime right outside the players' Viewport (whether the player moves to the right or to the left) with the following code:
Code: ags
function RandomItem()
{
    // Choose which item to place
    int r = Random(2);
	
    // Check if any item is already in the viewport
    // if NOT, put one
    if (!isItemInViewport())
    {
        // Make all the items different than r invisible
        for (int i = 0; i <= 2; i++)
        {
            if (i != r)
	    {
                object[i].Transparency = 100;
	    }
        }

	// Check which direction is the player taking
	if (player.DestinationX < player.x) // LEFT
	{
	    object[r].SetPosition(playerViewportEdgeLeft-Game.SpriteWidth[object[r].Graphic], 278); // Then move it right outside the viewport
	    object[r].Transparency = 0;
	}
	else if (player.DestinationX > player.x) // RIGHT
	{
	    object[r].SetPosition(playerViewportEdgeRight+Game.SpriteWidth[object[r].Graphic], 278);
	    object[r].Transparency = 0;
	}
    }
}


Thanks in advance, everybody :)
#8
I've been playing around with the Description module by SSH in order to achieve this:



In other words: when the cursor reaches the far right end of the screen, invert the position of the Hotspot Name GUI, placing it to the left of the cursor, instead of the right.

To do this I tweaked a specific line in the original module's script, from this:
Code: AGS

// Original code
protected function Descriptions::Update_Position(int font, int width)
{
    // ...

    this.x = mouse.x + this.OffsetX;

    if ((this.x + this.width) > this.MaxX)
    {
	this.x = this.MaxX - this.width;
    }

    // ...
}


To this:
Code: AGS

// My code
protected function Descriptions::Update_Position(int font, int width)
{
    // ...

    this.x = mouse.x + this.OffsetX;

    if ((this.x + this.width) > this.MaxX)
    {
        this.x = mouse.x - this.width - ((this.width/2) - this.OffsetX);
    }
    
    // ...
}


This mostly works but, as soon as I reach the top-right end of the screen (whether it's in windowed or fullscreen mode), the game crashes giving me the error: GUI.X co-ordinates specified are out of range.
I'm pretty sure I'm missing something in the messy "logic" I tried to find out. Maybe it's just the order of the operands in the expression, but I'm not really sure about that.

Does any of you have any advice on how to fix this?
Thanks in advance :-D
#9
I've been playing around, experimenting with the amazing Tween Module and I'm trying to create a custom Speech GUI that I'd like to see behave somewhat like this:

  • The actual GUI box should appear (rolling out, if you like) from the bottom to the top over the character's head, just like the LucasArts Speech does.
  • Other than the text wrapping, also the GUI Box's size should always be "aware" of the speaking character's position relatively to the screen, stretching to avoid going out of bounds
  • Could also be non-blocking
Is any of these unachieavable? The least important for me is the first one, anyway. I could easily discard it without problems and just have an instantly appearing GUI Box.

My own answers about the matter:

  • Maybe yes. I figured that you can only do the other way around, being the x=0 and y=0 "hardwired" to be placed at the top-left. I know how to place it over a character's head, at least (laugh)
  • Maybe not. Probably using the System.ViewportWidth and System.ViewportHeight, but I frankly have no clue about how to manage this properly (I suck at geometry!)
  • Maybe not. But maybe involves more, harder stuff like surfaces and timers
Does any of you have some solutions for these?
As always, thanks a million in advance!
#10
Hi there!

We're finishing the talkie version of Fanbots and we need 1 female voice actor and 1 male voice actor, for just 2 lines and 1 line respectively.

There is reference available, but the only real requirement is to be able to perform with a strong Australian accent :)

If you want to apply, send me a PM :D
Thanks in advance!
#11
Up until now I've always worked on small projects (both using AGS and other engines, but I'll try to stick with AGS here). The plan is to push the scope at least a little further, this time.

Plenty of AGSers have worked on relatively long games and I'm really curious to know what they suggest doing when working on a large project.

Being a not-so-expert programmer helps me to get things done but I often fail in motivation after I have to break the "flow" - for whatever reason - and get back to it afterwards. I think I could definitely use some tips on how to keep things tidy and easy to control and improve. Anybody can join this and\or ask for more, but for starters I'll ask some questions myself.

How do you design the puzzle structure (something I've been trying to study for a while, now) of your games?

Because of the kind of small projects I've worked on (mostly jam games) and also, I guess, because of the "scripted nature" of adventure games, I usually end up designing just an outline and then I "go with the flow". In other words: I know how things start, somehow know how things will end, but often I have little-to-no clue about what will happen in the middle. I found out that Jonathan Blow has a similar approach, so I felt less "stupid". Of course, this may work in his games' own contexts, not necessarily in everyone else's. EDIT: To me this applies when I create a room (or a set of rooms) with X objects in it and then ask myself what could be an interesting way to give those objects an actual purpose.

How do you organize your AGS projects?

The obvious downside of this approach is that I lose myself in countless lines. For instance: do you create specific script files for each cutscene then import it in the GlobalScript? Do you do the same for a group of Inventory Items or for each Character?

Even though I use the fabulous Dialog Designer by AJA, I also get lost in dialog branches quite a lot, which is ironic since I always enjoy trying to make realistic and interesting dialogs. Switching options off and on again, changing bool variables (e.g. didXaskYaboutZ), moving characters on screen to create a more "dramatic effect", etc. becomes difficult to test the more you get into it.


Thanks in advance to all those who will join this conversation, I hope that this will help me as well as others :D
#12
Hello everybody!

I'm trying to achieve what I think is a simple thing. I'm using AGS v3.4.0.6.

The game I'm working on has a single save slot. Whenever I hit the save button, I'd like the game to be saved at that slot and display "Game Saved!". The message will disappear after few seconds.
Then, when I restore that save, I'd like to display "Game loaded!".

In the documentation, it says that with SaveGameSlot() "The game will not be saved immediately; instead, it will be saved when the script function finishes executing".

At the moment I have this code in my GlobalScript.asc:
Code: ags

Overlay* myOverlay;

function repeatedly_execute_always() 
{
    if (IsTimerExpired(20))
    {
        myOverlay.Remove();
    }
}

function on_key_press(eKeyCode keycode) 
{
    if (keycode == eKeyF5)
    {
        SetTimer(20, 30);
        SaveGameSlot(1, "SaveGame");
        myOverlay = Overlay.CreateTextual(5, 5, 150, eFontfntTiny, 65535, "Game saved!");
    }

    if (keycode == eKeyF6)
    {
        RestoreGameSlot(1);
	SetTimer(20, 30);
        myOverlay = Overlay.CreateTextual(5, 5, 150, eFontfntTiny, 65535, "Game loaded!");
    }
}

Now, since SaveGameSlot() only saves when the script function finishes executing, where should I put the Overlay code?

At the moment, when I restore the game it still carries on the "Game saved!" overlay.

Thanks in advance! :)


#13
I called it Treasure Chest for the lack of a better name (laugh)
Some of you may already know but, anyway...

Basically, Aric Wilmunder (of SCUMM fame) recently began uploading a lot of amazingly interesting stuff on his website such as game pitches, proposals and design documents. This collections already includes games such Eidolon, Indiana Jones and The Last Crusade, Maniac Mansion (probably already available through Ron Gilbert's website), early Monkey Island versions and also the SCUMM Manual, which I'm really really curious about.

Have a look for yourself and enjoy :)
#14
FANBOTS
Another MAGS entry by LostTrainDude, Kastchey and CaptainD


A team of robots reaches the far planet of Koruuna to take over an abandoned Production Studio and revive their favourite show: ANDROIDS

DOWNLOAD

Features

  • Full Voice Acting by CaptainD and LostTrainDude! *NEW*
  • Custom Resolution (595x385) just because!
  • Isometric Graphics by Kastchey, because why not!
  • Robot lingo!
  • References!
  • NO COMMON INVENTORY!
  • A song performed by CaptainD!

How To Play

  • Left Click to Interact
  • Right Click to Examine



Changelog

v2.0.1 (August 2, 2016):

  • Implemented a proper message when choosing to restore game without having ever saved it. (thanks Amayirot Akago!)

v2.0 Update (August 2, 2016):

  • Full Voice Acting by CaptainD and LostTrainDude!
  • New, enhanced GUI!
  • A Start Menu with an animated background!
  • Enhanced Sound Effects!
  • Bugfixes!



Credits

  • LostTrainDude: Design, Programming, Voice Acting
  • Kastchey: Design, Art
  • CaptainD: Design, Sound Effects, Singing, Voice Acting



"Androids" is a fictional/parody TV series appearing for few brief moments in the series Red Dwarf
#15
Out of curiosity and will to become better at adventure game design (and, why not, maybe game design in general), I've started a little experiment in which I try to deconstruct the adventure games I play by writing down Puzzle Dependency Charts (as described by Ron Gilbert) and sharing my thoughts here and there.

I thought it could be interesting for you folks out here to see :) Hope you'll enjoy!

I'll keep a list here, that I'll update whenever I'll post a new one!

#1 - Dead Synchronicity: Tomorrow Comes Today
#2 - Broken Age
#3 - Zak McKracken: Between Time and Space
#4 - Technobabylon



To anybody who may be asking: to develop these charts I'm using yEd which is free (also for commercial use).
#16
I've just read this post on Thimbleweed Park's blog and found it very interesting.

TL;DR - Just like the old LucasFilm times, Ron Gilbert is implementing an "autoplay mode" where the game plays itself. At the time he and Aric Wilmunder made that to help themselves while showcasing, but turned out to be also a valuable testing tool.

I genuinely never thought about such thing for showcasing (which probably may even look odd if not scripted properly) and sure enough I also never imagined it applied to adventure games testing.

Did any of you ever try such thing with AGS, especially those of you who worked on large projects?
#17
I was trying to create a "space map" with randomly placed planets (as pixels, not as sprites) on it.

My goal is to make the mouse show which planet is the player looking at when moving onto it. It kinda works, but for some reason the pixel on the screen and the stored coordinates don't match.
The problem seems to occur only on the Y axis, anyway, which appears to be exactly +40 pixels from the actual pixel drawn onto the screen.



This is the whole code I've written.
I don't know if it's an useful information but I'm using AGS 3.3.3 and a 320x200 resolution.

Galaxy Map Header
Code: ags
//GalaxyMap.ash

struct PlanetStruct {
    int coord[2]; //for storing X and Y coordinates
    DrawingSurface* s;
};

import PlanetStruct Planet[5];


Galaxy Map Script
Code: ags
//GalaxyMap.asc

PlanetStruct Planet[5];

function repeatedly_execute_always()
{
    // This is the script that shows both the current mouse.x and mouse.y
    // AND a Display text when hitting a planet's coordinates

    String x = String.Format("%d, %d", mouse.x,  mouse.y);
    player.SayBackground(x);
    if (player.Room == 1) {
        int n = 0;
	while (n < 5) {
            if (mouse.x == Planet[n].coord[0] && mouse.y == Planet[n].coord[1]) Display("Planet: %d", n);
            n++;
	}
    }
}

export Planet;


Room Script
Code: ags
//room1.asc

// Creates a given amount of planets, randomly placed over the screen
// Stores their X and Y coordinates and displays them next to them
function PlanetForming(int totalPlanet)
{
    int n = 0;
    while (n < totalPlanet) {
	Planet[n].s = Room.GetDrawingSurfaceForBackground();
        // Probably ultra-spaghetti. I was trying to keep the pixels a little far from the corners of the screen.
	int rX = Random(Room.Width-10)+10;
	int rY = Random(Room.Height-10)+10; // Same as above
	
	Planet[n].s.DrawingColor = 15;
	Planet[n].s.DrawPixel(rX, rY);
	Planet[n].s.Release();
	
	Planet[n].coord[0] = rX; // Store the X coordinate of the drawn pixel
	Planet[n].coord[1] = rY; // Store the Y coordinate of the drawn pixel

        // Next to each planet, write it's current x and y coordrinates
	String st = String.Format("%d, %d", rX, rY);
	DrawingSurface* t = Room.GetDrawingSurfaceForBackground();
	t.DrawingColor = 15;
	t.DrawStringWrapped(rX, rY, 25, eFontfntTiny, eAlignCentre, st);
	t.Release();
	n++;
    }	
}

function room_Load()
{
    PlanetForming(5);
}


With no clue whatsoever, I've also tried using the UseHighResCoordinates, but without luck.

Thanks in advance, even more if the matter is easy to solve and I'm just bad at this :D
#18
Hello!

I'm trying to get a "light blinking randomly" effect to work in a specific room.
To do that, I'm trying to create a recursive timer that just switches the background and lasts a different random (float) amount of seconds each time it's triggered.

To generate a random float number I'm using the RandomFloat function described in this thread.
Also, I'm using the Tween Module as well.

The code is structured in the following way:

In Global Variables, I've put a float variable to store the timer's time (float randLightTime = 0.0)

In my GlobalScript.asc:
Code: ags

// At the very top of the file
float RandomFloat(float max) {
  int huge = 2147483647;
  float f = IntToFloat(Random(huge)) / IntToFloat(huge);  // 0.0 - 1.0
  return f * max;
}

function repeatedly_execute_always()
{
  if (cEgo.Room == 2) {
    if (IsTimerExpired(9)) {
      SetBackgroundFrame(GetBackgroundFrame()-1);
      randLightTime = RandomFloat(2.5);
      SetTimer(9, SecondsToLoops(randLightTime));
    }
  }
}


The timer is set in the room where this blinking light should be working (Room 2):
Code: ags

function room_Load()
{
  SetTimer(9, SecondsToLoops(1.0));
}


I'm getting an error that involves both line 4 and line 13 in the GlobalScript.asc.
When I enter the room the game crashes and I get this message: "Random: invalid parameter passed -- must be at least 0".

I tried starting the timer in the game_start function and also tried changing the initial randLightTime value, without luck.
Am I missing something really stupid?

Thanks in advance!
#19
[imgzoom]http://i.imgur.com/TMDAd30.png]http://i.imgur.com/c3eb0LF.png[/imgzoom]
A LANDLORD'S DREAM

Abel Lowen, a Stringshaper, gets woke up by his apartment's alarm clock in the middle of the night. When he'll finally get up, late for his band's rehearsal, he will find out that almost nothing seems to be working properly.

What happened? Are others experiencing the same problems? Why is his neighbour hallucinating in the middle of the hallway?


HOW TO PLAY

  • LEFT CLICK - Interact
  • RIGHT CLICK - Examine
  • F5 - Save Game
  • F6 - Load Game

FEATURES

  • 320x200 resolution
  • Sci-fi setting
  • Sci-fi inspired music
  • A rare example of good neighbours' adventuring!

TEAM

  • LostTrainDude - Design, Coding, Music
  • Kastchey - Art, Design
  • CaptainD - Brainstorming Help, SFX, Proofreading

21/03/16: To celebrate the game's nomination for Best Music and Sound at the AGS Awards 2015, the short OST is now available for download!
#20
Some people have noticed me that they had some difficulties playing my latest MAGS entry because of the brightness.

EDIT: I'm using AGS 3.3.0. Didn't mention it before.

I tried to bind System.Gamma adjustment to two different keys (i.e. eKey4 and eKey5) and also tried to implement a slider within a GUI (where System.Gamma = sldGamma.Value).

The game has a 800x600 resolution with 32-bit colours.

This is my GlobalScript code. While it seems that the code itself is working, the game won't show not even the slightest change in brightness (of course the game is full screen).

Am I missing something? Or did I just mess up something with the sprite\background imports (all PNG files, by the way)?

Code: ags

//GlobalScript.asc

function on_key_press(eKeyCode keycode) {
    if (keycode == eKey4) {
        if (System.SupportsGammaControl) {
            cEgo.Say("It works!"); // Just to test if it works or not
            if (System.Gamma > 0) { // if System.Gamma is less than 0 the game crashes
                System.Gamma -= 5;
            }
        } else {
          cEgo.Say("It doesn't work!"); // Just to test if it works or not
        }
    }

    if (keycode == eKey5) {
        if (System.SupportsGammaControl) {
            cEgo.Say("It works!"); // Just to test if it works or not
            if (System.Gamma < 200) { // if System.Gamma is greater than 200 the game crashes
                System.Gamma += 5;
            }
        } else {
          cEgo.Say("It doesn't work!"); // Just to test if it works or not
        }
    }
}


I think that maybe is related to the Colour depth, since I found that there are some issues with that in some areas of the documentation. For instance:
QuoteAdvanced Room Features > Lighting effects
Light levels only work when the character's graphic is at the same colour depth as the background (ie. a 256-colour character in a hi-colour game won't get lightened).

Thanks a million!
SMF spam blocked by CleanTalk