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 - eri0o

#361
So, I wanted to see if it was possible to do Wave Function Collapse in AGS Script. Because I was lazy, I decided to try to port the code of this C version here: https://github.com/krychu/wfc/blob/master/wfc.h

Now this did NOT work, it runs and takes some long time, but the result at the end is wrong... Anyhow, decided to leave my try it out here in case someone really wants to take the challenge. Probably the best approach is to actually read the papers and figure a strategy that is better suited for the concepts available in AGS.

wfc.ash
Code: ags
// new module header

#define MAX_TILES 1024

enum WfcDirection {
  eWfcDir_Up = eDirectionUp, 
  eWfcDir_Down = eDirectionDown, 
  eWfcDir_Left = eDirectionLeft, 
  eWfcDir_Right = eDirectionRight 
};

enum WfcMethod {
  eWfc_Overlapping, 
  eWfc_Tiled, 
};

managed struct WfcCell {
  int Tile[MAX_TILES];                  // Possible tiles in the cell (initially all)
  int TileCount;

  int FrequencySum; // Sum of tile frequencies used to calculate
                    // entropy and randomly pick a tile when
                    // collapsing a tile.

  float Entropy;    // Shannon entropy. Cell with the smallest entropy
                    // is picked to be collapsed next.
};

managed struct WfcProp {
  int SrcCellIdx;
  int DstCellIdx;
  WfcDirection direction;
};

struct Wfc {
  WfcMethod method;
  int seed;
  
  DynamicSprite* Input;
  int TileWidth;
  int TileHeight;  
  bool FlipTilesX;
  bool FlipTilesY;
  bool RotateTiles;
  
  DynamicSprite* TilesSprite[];
  int TilesFrequency[]; // count of tile occurrences in the input image.
  int TilesCount;
  
  /* output */
  
  DynamicSprite* Output;
  WfcCell* Cell[];
  int CellCount;
  int FrequencySum;
  
  /* in-use */
  
  WfcProp* Prop[];
  int PropCount;
  int PropIdx;
  
  int CollapsedCellCount;
  
  bool allowed_tiles[1048576];
  
  
  /* API */
  
  import void Init(DynamicSprite* output, int input_sprite, int tile_width, int tile_height, bool xflip_tiles = true, bool yflip_tiles = true, bool rotate_tiles = true, bool expand_input = false);
  import void Restart();
  import void RegenerateOutput();
  import void Run(int max_collapse_cnt);
  import void RepeatedlyUpdate(int times = 1);
  bool Generated;
  
  protected int _Rp_CellIdx;
  protected int _Rp_MaxCollapseCnt;
  
  import protected void _SwapTiles(int a_idx, int b_idx);  
  import protected void _RemoveDuplicatedTiles();
  
  import protected void _CreateProps();
  import protected void _CreateCells();
  import protected void _CreateTiles();
  
  import protected void _AddProp(int src_cell_idx, int dst_cell_idx, WfcDirection direction);
  import protected void _AddPropUp(int src_cell_idx);
  import protected void _AddPropDown(int src_cell_idx);
  import protected void _AddPropLeft(int src_cell_idx);
  import protected void _AddPropRight(int src_cell_idx);
  import protected bool _IsTileEnabled(int tile_idx, int cell_idx, WfcDirection direction);
  import protected bool _IsPropPending(int cell_idx, WfcDirection direction);
  
  import protected bool _PropagateProp(WfcProp* p);
  import protected bool _Propagate(int cell_idx);
  import protected bool _Collapse(int cell_idx);
  import protected int _NextCell();
  import protected void _InitCells();
   
  import protected void _ComputeAllowedTiles();
  import protected void _CreateTilesOverlapping(int tile_width, int tile_height, bool expand_image, bool xflip_tiles, bool yflip_tiles, bool rotate_tiles);
};

wfc.asc
Code: ags
// new module script

int _DirToIdx( WfcDirection direction)
{
  switch (direction) {
    case eWfcDir_Up:    return 0; break;
    case eWfcDir_Down:  return 1; break;
    case eWfcDir_Left:  return 2; break;
    case eWfcDir_Right: return 3; break;
  }
  return 0;
}

WfcDirection _IdxToDir(int d)
{
  switch (d) {
    case 0: return eWfcDir_Up; break;
    case 1: return eWfcDir_Down; break;
    case 2: return eWfcDir_Left; break;
    case 3: return eWfcDir_Right; break;
  }
  return eWfcDir_Up;
  
}

// Return 1 if the two images overlap perfectly except the edges in the given direction, 0 otherwise.
bool _SprCmpOverlap(DynamicSprite* dynspr_a, DynamicSprite* dynspr_b, WfcDirection direction)
{
  int a_offx, a_offy, b_offx, b_offy, width, height;

  switch (direction) {
    case eWfcDir_Up:
      a_offx = 0; a_offy = 0;
      b_offx = 0; b_offy = 1;
      width = dynspr_a.Width;
      height = dynspr_a.Height-1;
      break;
    case eWfcDir_Down:
      a_offx = 0; a_offy = 1;
      b_offx = 0; b_offy = 0;
      width = dynspr_a.Width;
      height = dynspr_a.Height-1;
      break;
    case eWfcDir_Left:
      a_offx = 0; a_offy = 0;
      b_offx = 1; b_offy = 0;
      width = dynspr_a.Width-1;
      height = dynspr_a.Height;
      break;
    case eWfcDir_Right:
      a_offx = 1; a_offy = 0;
      b_offx = 0; b_offy = 0;
      width = dynspr_a.Width-1;
      height = dynspr_a.Height;
      break;
    default:
      AbortGame("WFC Error: Invalid Direction");
      return false;
  }

  DrawingSurface* surf_a = dynspr_a.GetDrawingSurface();
  DrawingSurface* surf_b = dynspr_b.GetDrawingSurface();

  for (int y=0; y<height; y++) {
    for (int x=0; x<width; x++) {

      int a_x = a_offx + x;
      int b_x = b_offx + x;
      int a_y = a_offy + y;
      int b_y = b_offy + y;

      if(surf_a.GetPixel(a_x, a_y) != surf_b.GetPixel(b_x, b_y)) {
        return false;
      }
    }
  }
  
  surf_a.Release();
  surf_b.Release();
  
  return true;
}

 bool noloopcheck _SprEqual(DynamicSprite* dynspr_a, DynamicSprite* dynspr_b)
{
  if(dynspr_a.Width != dynspr_b.Width || dynspr_a.Height != dynspr_b.Height) {
    return false;
  }
  
  if(dynspr_a == dynspr_b) return true;
  
  int width = dynspr_a.Width;
  int height = dynspr_a.Height;
  
  DrawingSurface* surf_a = dynspr_a.GetDrawingSurface();
  DrawingSurface* surf_b = dynspr_b.GetDrawingSurface();
  
  for (int y=0; y<height; y++) {
    for (int x=0; x<width; x++) {
      if(surf_a.GetPixel(x, y) != surf_b.GetPixel(x, y)) {
        return false;
      }
    }
  }
  
  return true;
}

DynamicSprite* noloopcheck _GenerateOutput(int width, int height,  WfcCell* cells[], DynamicSprite* tiles[])
{
  DynamicSprite* spr = DynamicSprite.Create(width, height);
  DrawingSurface* surf = spr.GetDrawingSurface();
  
  for (int y=0; y<height; y++) {
    for (int x=0; x<width; x++) {
      WfcCell* cell = cells[y*width+x];
      
      int component = 0;
      for(int i=0; i<cell.TileCount; i++) {
        DynamicSprite* tile = tiles[cell.Tile[i]];
        DrawingSurface* tmp_surf = tile.GetDrawingSurface();
        component += tmp_surf.GetPixel(0, 0);
        tmp_surf.Release();
      }
      
      surf.DrawingColor = component/cell.TileCount;
      surf.DrawPixel(x, y);
    }
  }
  
  surf.Release();
  return spr;
}

void _AddOverlappingImages(DynamicSprite* tiles_dynspr[], int tiles_freq[], DynamicSprite* img, int xcnt, int ycnt, int tile_width, int tile_height)
{
  int tile_cnt = xcnt * ycnt;
  DrawingSurface* surf = img.GetDrawingSurface();
  
  for (int y=0; y<ycnt; y++) {
    for (int x=0; x<xcnt; x++) {
      int i = y*xcnt + x;
      
      tiles_freq[i] = 1;
      tiles_dynspr[i] = DynamicSprite.CreateFromDrawingSurface(surf, x, y, tile_width, tile_height);
    
    }
  }
}


void _AddFlippedImages(DynamicSprite* tiles_dynspr[], int tiles_freq[], int tile_idx, eFlipDirection flip_dir)
{
  for (int i=0; i<tile_idx; i++) {
    int src_idx = i;
    int dst_idx = tile_idx + i;
    
    tiles_freq[dst_idx] = 1;
    DynamicSprite* spr_src = tiles_dynspr[src_idx];
    tiles_dynspr[dst_idx] = DynamicSprite.CreateFromExistingSprite(spr_src.Graphic, true);
    DynamicSprite* spr_dst = tiles_dynspr[dst_idx];
    spr_dst.Flip(flip_dir);    
  }
}


void _AddRotatedImages(DynamicSprite* tiles_dynspr[], int tiles_freq[], int tile_idx)
{
  
  for (int i=0; i<tile_idx; i++) {
    int src_idx = i;
    DynamicSprite* src_spr = tiles_dynspr[src_idx];
    
    for (int j=0; j<3; j++) {
      
      int dst_idx = tile_idx + i*3 + j;
      tiles_freq[dst_idx] = 1;
      
      tiles_dynspr[dst_idx] = DynamicSprite.CreateFromExistingSprite(src_spr.Graphic, true);
      DynamicSprite* spr_dst = tiles_dynspr[dst_idx];
      
      if(j != 0) {
        spr_dst.Rotate(j*90);
      }
    }
  }
}

protected void Wfc::_SwapTiles(int a_idx, int b_idx)
{
  int tmp_freq = this.TilesFrequency[a_idx];
  DynamicSprite* tmp_spr = this.TilesSprite[a_idx];
  
  this.TilesFrequency[a_idx] = this.TilesFrequency[b_idx];
  this.TilesSprite[a_idx] = this.TilesSprite[b_idx];
  
  this.TilesFrequency[b_idx] = tmp_freq;
  this.TilesSprite[b_idx] = tmp_spr;
}

protected void Wfc::_RemoveDuplicatedTiles()
{
  int unique_cnt = 1;
  for (int j=1; j<this.TilesCount; j++) {
    bool unique = true;
    for (int k=0; k<unique_cnt; k++) {
      if(_SprEqual(this.TilesSprite[j], this.TilesSprite[k])) {
        unique = false;
        this.TilesFrequency[k]++;
        break;
      }
    }
    
    
    if (unique) {
      if (unique_cnt != j) {
        this._SwapTiles(j, unique_cnt);        
      }
      
      unique_cnt++;
    }
  }

  for(int i=unique_cnt; i<this.TilesCount; i++) {
    this.TilesFrequency[i] = 0;
    this.TilesSprite[i] = null;
  }
  
  this.TilesCount = unique_cnt;
}

#define WFC_MAX_PROP_CNT 1000

////////////////////////////////////////////////////////////////////////////////
//
// WFC: Solve (Method-independent)
//
////////////////////////////////////////////////////////////////////////////////


protected void noloopcheck Wfc::_CreateProps()
{
  int prop_max = this.CellCount * WFC_MAX_PROP_CNT;
  this.Prop = new WfcProp[prop_max];
  
  for(int i=0; i<prop_max; i++)
  {
    this.Prop[i] = new WfcProp;
  }
}

protected void noloopcheck Wfc::_CreateCells()  
{
  int cell_cnt = this.CellCount;
  this.Cell = new WfcCell[cell_cnt];
  
  for (int i=0; i<cell_cnt; i++)
  {
    WfcCell* cell = new WfcCell;
    this.Cell[i] = cell;

    if(i==0) continue;
    
    int tile_count = this.TilesCount;
    
    cell.TileCount = tile_count;
  }
}

protected void Wfc::_CreateTiles()
{
  int tile_cnt = this.TilesCount;
  this.TilesFrequency = new int[tile_cnt];
  this.TilesSprite = new DynamicSprite[tile_cnt];
  
  for (int i=0; i<this.TilesCount; i++) {
    this.TilesFrequency[i] = 0;
    this.TilesSprite[i] = null;
  }
}
  
protected void Wfc::_AddProp(int src_cell_idx, int dst_cell_idx, WfcDirection direction)
{
  WfcProp* prop = this.Prop[this.PropCount];
  
  prop.SrcCellIdx = src_cell_idx;
  prop.DstCellIdx = dst_cell_idx;
  prop.direction = direction;
  
  this.PropCount++;
}

protected void Wfc::_AddPropUp(int src_cell_idx)
{
  int dst_cell_idx = src_cell_idx - this.Output.Width;
  if(dst_cell_idx < 0) return; // can't go to upper cell,  in first line
  
  this._AddProp(src_cell_idx, dst_cell_idx, eWfcDir_Up);
}

protected void Wfc::_AddPropDown(int src_cell_idx)
{
  int dst_cell_idx = src_cell_idx + this.Output.Width;
  
  if(dst_cell_idx > this.CellCount) return; // can't go to down cell,  in last line
  
  this._AddProp(src_cell_idx, dst_cell_idx, eWfcDir_Down);    
}

protected void Wfc::_AddPropLeft(int src_cell_idx)
{
  int dst_cell_idx = src_cell_idx - 1;
  
  if(src_cell_idx % this.Output.Width == 0) return;
  
  this._AddProp(src_cell_idx, dst_cell_idx, eWfcDir_Left);   
}

protected void Wfc::_AddPropRight(int src_cell_idx)
{
  int dst_cell_idx = src_cell_idx + 1;
  
  if(src_cell_idx % this.Output.Width == this.Output.Width - 1) return;
    
  this._AddProp(src_cell_idx, dst_cell_idx, eWfcDir_Right);
}

protected bool Wfc::_IsTileEnabled(int tile_idx, int cell_idx, WfcDirection direction)
{
  WfcCell* cell = this.Cell[cell_idx];
  int tile_cnt = this.TilesCount;

  for(int i=0, cnt=cell.TileCount; i<cnt; i++)
  {
    if(this.allowed_tiles[_DirToIdx(direction)*this.TilesCount+tile_idx]) return true;
  }
  
  return false;
}

// Checks whether particular prop is already added and pending, in which
// case there is no point of adding the same prop again.
protected bool Wfc::_IsPropPending(int cell_idx, WfcDirection direction)
{
  for (int i=this.PropIdx+1; i < this.PropCount; i++) {
    WfcProp* prop = this.Prop[i];
    
    if(prop.SrcCellIdx == cell_idx && prop.direction == direction) return true;    
  }
  
  return false;
}


// Updates tiles in the destination cell to those that are allowed by the source cell
// and propagate updates
protected bool Wfc::_PropagateProp(WfcProp* p)
{
  int new_cnt = 0;
  
  WfcCell* dst_cell = this.Cell[p.DstCellIdx];
  
  // Go through all destination tiles and check whether they are enabled by the source cell
  for (int i=0, cnt=dst_cell.TileCount; i<cnt; i++) {
    int possible_dst_tile_idx = dst_cell.Tile[i];
    
    // If a destination tile is enabled by the source cell, keep it
    if(this._IsTileEnabled(possible_dst_tile_idx, p.SrcCellIdx, p.direction)) {
      dst_cell.Tile[new_cnt] = possible_dst_tile_idx;
      new_cnt++;
    } else {
      int freq = this.TilesFrequency[possible_dst_tile_idx];
      float p_f = IntToFloat(freq) / IntToFloat(this.FrequencySum);
      
      dst_cell.Entropy += p_f*Maths.Log(p_f);
      dst_cell.FrequencySum -= freq;
      
      if(dst_cell.FrequencySum == 0) return false;
    }
  }
  
  if(new_cnt == 0) return false;
  
  if(dst_cell.TileCount != new_cnt) {
    int dst_cell_i = p.DstCellIdx;
    if(new_cnt == 1) this.CollapsedCellCount++;
    if(p.direction != eWfcDir_Down && this._IsPropPending(dst_cell_i, eWfcDir_Up)) this._AddPropUp(dst_cell_i);
    if(p.direction != eWfcDir_Up && this._IsPropPending(dst_cell_i, eWfcDir_Down)) this._AddPropDown(dst_cell_i);
    if(p.direction != eWfcDir_Right && this._IsPropPending(dst_cell_i, eWfcDir_Left)) this._AddPropLeft(dst_cell_i);
    if(p.direction != eWfcDir_Left && this._IsPropPending(dst_cell_i, eWfcDir_Right)) this._AddPropRight(dst_cell_i);    
  }
  
  dst_cell.TileCount = new_cnt;
  
  return true;
}

protected bool Wfc::_Propagate(int cell_idx)
{
  this.PropCount = 0;
  
  this._AddPropUp(cell_idx);
  this._AddPropDown(cell_idx);
  this._AddPropLeft(cell_idx);
  this._AddPropRight(cell_idx);
  
  for(int i=0; i<this.PropCount; i++)
  {
    this.PropIdx = i;
    WfcProp* p = this.Prop[i];
    
    if(!this._PropagateProp(p)) {
      return false;
    }
  }
  
  return true;
}


protected bool Wfc::_Collapse(int cell_idx)
{
  WfcCell* cell = this.Cell[cell_idx];
  
  int remaining = Random(cell.FrequencySum);
  
  for(int i=0; i<cell.TileCount; i++) {
    int tile_idx = cell.Tile[i];
    int freq = this.TilesFrequency[tile_idx];
    
    if(remaining >= freq) {
      remaining -= freq;
    } else {
      cell.Tile[0] = cell.Tile[i];
      cell.TileCount = 1;
      cell.FrequencySum = 0;
      cell.Entropy = 0.0;
      this.CollapsedCellCount++;
      return true;
    }
  }
  
  return false;  
}

protected int Wfc::_NextCell()
{
  int min_idx = -1;
  float min_entropy = 3402823466.0;
  
  for (int i=0; i<this.CellCount; i++) {    
    WfcCell* cell = this.Cell[i];
    
    // Add small noise to break ties between tiles with the same entropy
    float entropy = cell.Entropy + (IntToFloat(Random(4096))/8388608.0);
    
    
    if (cell.TileCount != 1 && entropy < min_entropy) {
      min_entropy = entropy;
      min_idx = i;
    }
  }
  
  return min_idx;
}

protected void noloopcheck Wfc::_InitCells()
{
  int sum_freqs = 0;
  
  for(int i=0; i<this.TilesCount; i++) {
    sum_freqs += this.TilesFrequency[i];
  }
  
  this.FrequencySum = sum_freqs;
  
  float sum_plogp = 0.0;
  for (int i=0; i<this.TilesCount; i++) {
    float p_f = IntToFloat(this.TilesFrequency[i])/IntToFloat(sum_freqs);
    sum_plogp += p_f*Maths.Log(p_f);
  }
  
  float entropy = -sum_plogp;
  
  for(int i=0; i<this.CellCount; i++)
  {
    WfcCell* cell = this.Cell[i];
    
    cell.TileCount = this.TilesCount;
    cell.FrequencySum = sum_freqs;
    cell.Entropy = entropy;
    
    for(int j=0; j<this.TilesCount; j++) {
      cell.Tile[j] = j;
    }    
  }
  
  this.PropCount = 0;
}

protected void noloopcheck Wfc::_ComputeAllowedTiles()
{
  int tile_cnt = this.TilesCount;
  
  for (int d=0; d<4; d++) {
    for (int i=0; i<tile_cnt; i++) {
      for (int j=0; j<tile_cnt; j++) {
        int idx = d*i*tile_cnt+j;
        
        this.allowed_tiles[idx] = _SprCmpOverlap(this.TilesSprite[i], this.TilesSprite[j], _IdxToDir(d));
      }
    }
  }
}

protected void Wfc::_CreateTilesOverlapping(int tile_width, int tile_height, bool expand_image, bool xflip_tiles, bool yflip_tiles, bool rotate_tiles)
{
  int xcnt = this.Input.Width - tile_width + 1;
  int ycnt = this.Input.Height - tile_height + 1;
  
  if(expand_image) {
    xcnt = this.Input.Width;
    ycnt = this.Input.Height;
    int w = this.Input.Width;
    int h = this.Input.Height;
    this.Input.Resize(w + tile_width - 1, h + tile_height -1);
  }
  
  int tile_cnt = xcnt*ycnt;
  
  if(xflip_tiles) tile_cnt *= 2;    
  if(!(xflip_tiles && rotate_tiles) && yflip_tiles) tile_cnt *= 2;
  if(rotate_tiles) tile_cnt *= 4;
  
  this.TilesCount = tile_cnt;
  this._CreateTiles();
  
  _AddOverlappingImages(this.TilesSprite, this.TilesFrequency, this.Input, xcnt, ycnt, tile_width, tile_height);
  
  int base_tile_cnt = xcnt * ycnt;
  
  if(xflip_tiles) {
    _AddFlippedImages(this.TilesSprite, this.TilesFrequency, base_tile_cnt, eFlipLeftToRight);
    base_tile_cnt *= 2;
  }
  
  if(!(xflip_tiles && rotate_tiles) && yflip_tiles) {
    _AddFlippedImages(this.TilesSprite, this.TilesFrequency, base_tile_cnt, eFlipUpsideDown);
    base_tile_cnt *= 2;
  }
  
  if(rotate_tiles) {
    _AddRotatedImages(this.TilesSprite, this.TilesFrequency, base_tile_cnt);
    base_tile_cnt *= 4;
  }
  
  this._RemoveDuplicatedTiles();
}

void Wfc::Restart()
{
  this.CollapsedCellCount = 0;
  this.Generated = false;
  this._InitCells();
}

void Wfc::Init(DynamicSprite* output, int input_sprite, int tile_width, int tile_height, bool xflip_tiles, bool yflip_tiles, bool rotate_tiles, bool expand_input)
{
  this.Output = output;
  this.Input = DynamicSprite.CreateFromExistingSprite(input_sprite, true);
  this.method = eWfc_Overlapping;
  this.CellCount = this.Output.Width * this.Output.Height;
  this.TileWidth = tile_width;
  this.TileHeight = tile_height;
  this.TilesCount = 0;
  this.FlipTilesX = xflip_tiles;
  this.FlipTilesY = yflip_tiles;
  this.RotateTiles = rotate_tiles;
  
  this._CreateTilesOverlapping(tile_width, tile_height, expand_input, xflip_tiles, yflip_tiles, rotate_tiles);
  
  this._ComputeAllowedTiles();
  
  this._CreateCells();
  this._CreateProps();
    
  this.Restart();
}

void Wfc::RegenerateOutput()
{
  DynamicSprite* out = _GenerateOutput(this.Output.Width, this.Output.Height, this.Cell, this.TilesSprite);
  
  DrawingSurface* surf = this.Output.GetDrawingSurface();
  
  surf.Clear();
  surf.DrawImage(0, 0, out.Graphic);
  surf.Release();
}

void Wfc::Run(int max_collapse_cnt)
{
  int cell_idx = Random(this.Output.Height * this.Output.Width);
  this._Rp_CellIdx = cell_idx;
  this._Rp_MaxCollapseCnt = max_collapse_cnt;
}

void Wfc::RepeatedlyUpdate(int times)
{
  for(int i=0; i<times; i++) {
    int cell_idx = this._Rp_CellIdx;
    int max_collapse_cnt = this._Rp_MaxCollapseCnt;
    
    if(cell_idx == -1 || this.CollapsedCellCount == max_collapse_cnt) {
      this.Generated = true;
      return;
    }
    
    if(!this._Collapse(cell_idx)) return;  
    if(!this._Propagate(cell_idx)) return;
    
    cell_idx = this._NextCell();
    this._Rp_CellIdx = cell_idx;
    
    if(cell_idx == -1 || this.CollapsedCellCount == max_collapse_cnt) {
      this.Generated = true;
      return;
    }
  }
}

Usage

Code: ags
Wfc wfc;
Overlay* ovr;

function room_RepExec()
{
  wfc.RepeatedlyUpdate(8192);
  
  if(wfc.Generated) {
    if(ovr == null) {
      wfc.RegenerateOutput();
      ovr = Overlay.CreateGraphical(32, 32, wfc.Output.Graphic, true);
    }
  }
}

function room_Load()
{
  wfc.Init(DynamicSprite.Create(48, 48, true), 1, 3, 3, true, true, true, false);
  wfc.Run(2048);
  
}

function room_AfterFadeIn()
{

}
#362
Hey @Snarky , if you wish, this specific 3.6.0.38, the MacOS build for release of the engine is here https://cirrus-ci.com/task/5718421195718656 , under binaries/bin_12.3_release, there should be an ags artifact, it should be able to run from the command line.  :)

#363
Just remove the SDL2.dll in the Compiled/Windows directory and add this new one and double click the game exe to trigger the crash again. :)
#364
The crash dump is also in the Compiled/Windows dir of the attached project in the previous link!

I find the alternate data stream file there (the TestEdge.exe_Zone.Identifier_$DATA) suspicious, and the exe file URL in it doesn't match the exe file location in the crash dump.  :-\

(the ZoneId being 3 is even more curious, it means the file came from the internet)
#365
It looks like it's failing at SDL_Init (sys_main.cpp line 42), but I don't have the kernelbase.dll file in my windows and I can't find it's symbols, so I don't know what the exception is - I think Snarky is in Windows 7?
#366
Some quick notes on the development

Almost two months ago a little cat appeared in my home, all hungry and thin. We have adopted him, and now he is healthy. He was with me the whole time of making the game - on my lap, or feet, quietly sitting near the monitor or trying to pull all my USB cables. He was the inspiration for making the game.

After I Rented a Boat, I wanted a game where you were able to look freely and smoothly, so a good time went into speeding things up. This was possible because Crimson Wizard improved the flexibility and performance of Overlays in AGS. Additionally, Khris's math from AGS Kart made the camera here possible. I early on decided the game would be locked in a forest with a home at the center as a return point between tasks. This small scope allowed me to polish the forest geometry to work best with the renderer.  In total, Haloa, Morgan, Heltenjon, Newwaveburritos, and Jon Paul Sapsford tested fourteen different versions of the game and kept me motivated!

In an initial sketch of the game scope, it was about photographing ghosts to show your owner why you were meowing, but I had a hard time scripting it. Haloa suggested guiding the ghosts home instead. This lead to the dialogues, which prevented an information dump at the start.

A lot of details arose from conversations with Morgan and Heltenjon, like the smoke in the house to help to find it and scratching things as breadcrumbs, navigation nudges in the forest with the flowers, plaque, and dandelions, and improving the dialogues so that the ghosts ask to be brought home. Newwaveburritos, who asked about adding fireflies, understood my vision of a game more about the atmosphere than the objectives and motivated me to improve the forest sounds.

During the middle of development, I listened to various music assets and couldn't find anything that sounded like it fit the game. Years ago, I worked with Jon Paul Sapsford on the soundtrack for Tea for Two, and the last thing he told me was to reach out if I was making a new game. So I did! He was lovely and quickly figured out what would work with the game. The final music was made before the game had an ending, so that scene's mood and timing are from it.

I wanted to do technical-oriented write-up, with some design too, but I am having trouble figuring out what to write, so if anyone has questions, please ask! Also, to whoever sent me the 2 USD in the game itch page, it made my night! Thank you!
#367
Thank you for the kind words Lorenzo! And the rating too ❤️ About the navigation on the forest there was some effort in making it feel natural and possible to navigate, but what really made it possible was the many builds of the game that the people testing relentlessly tried it out. I am working on a small little write-up to talk about it non-technically on this iterative design.

And yes, the chimney was intended to work as a way to guide you home, in a way that you should be able to get back there and reorient yourself before exploring again. It was in my mind a better way to accomplish what I intended with the radio tower in I rented a boat, but for me it had not really quite work there.

I briefly thought about having stars in the sky too, so that could work, but I could not figure any way to do it so it worked properly with everything and did no performance hit. As you noticed here, there was a focus on letting the game be fluid - my cat is young and he moves really fast, so I wanted that speed!  :-D
#368
It looks like the hotspot hExitToLake (or some other other thing) has an event that in the Editor is linked to a function named "exit to lake" but the function in room script is named hExit_WalkOn, so either go to the event panel of that hotspot (or some other) and delete the linked event "exit to lake" and click the three dots to create a new event in room script or if hExit_WalkOn is the intended function use that name - or if no such function is intended just leave it blank.
#369
So, I was more testing the waters in the "can it be done" sense... Like, can AGS take two thousand overlays and smoothly move them around and can the zordering work? Can we rotate all that? And yeah, it appears so. But properly figuring out a pseudo-3d like camera here, I haven't yet figured that...
#370
I made a quick update to add fireflies in my game. :)

Spoiler
[close]

I also pushed it in itchio and in the completed games page.
#371
Don't Give Up the Cat


Tonight, you are out in the woods. As a cat.

A short game intended to be played at night, using headphones. In this first-person game you use your mouse to look around and scratch things, and WASD keys for walking, shift to run, and you use Esc to show the menu.
After you run you may sit around to breathe a bit and listen to the forest.


Game by eri0o
Music by Jon Paul Sapsford
Ghosts, graves and icon by Haloa
Gameplay Testing by Morgan Willcock, Heltenjon and Newwaveburritos

AGS Script Modules
Tween by Edmundito
Timer and Typed Text by  Crimson Wizard

Additional assets from itch io, opengameart and freesound
Forest trees, stones, flowers and base palette: Trixie (trixelized) - STRINGSTAR FIELDS
Clouds: ansimuz - Sunnyland,  recolored
House: Lanthanum - 3D Model House (PicoCAD),  recolored
Save cat: kotnaszynce - kitka.gif,  recolored+resized
Smoke: KnoblePersona - Smoke & Fire Animated Particle
Title Screen Cat: Ben - Camp Cat (PicoCAD), modified
Forest crickets and atmosphere: LokiF - Swamp Environment Audio
Cat Footsteps: melle_teich - Animal footsteps on dry leaves
Cat Jump: FOX2814 - Cat jump
Game Font: Fibberish by  Nathan Scott

This is a MAGS entry for the theme "Ghost", see original submission here.

The OST
#372
Thanks for this @Snarky !! This page has something I have been trying to figure it out

QuoteWhen the camera is rotating, it's not that simple, cause things are sometimes in front or in the back depending on where the camera is, so we need some math magic. The dot product is used to check the similarity between the "up vector" and the position of the object:

z_index = -position.dot(up_vector)

But I don't know what that means yet!  (laugh)

Also, here's me changing the size of each overlay experimentally, to play with depth.

#373


A friend made me this. Twice. I am thinking about what to do with the extra one I got! It's 3D printed and coated with some paint, pixel blue cup and matching keychain-ags tea bag - it has the pixel blue cup from the manual on the other side of the keychain too!
#374


Hey, I am starting to play around with Sprite Stacking in AGS. Has someone ever done this here?

Sliced.ash
Code: ags
// new module header
#define MAX_SLICED_OBJECTS 32
#define MAX_OVERLAYS 2048
#define OVERLAYS_STEP 32

managed struct SlicedObject {
  import void Set(float x, float y, float z, int graphic_first_slice, int graphic_last_slice);
  import static SlicedObject* Create(float x, float y, float z, int graphic_first_slice, int graphic_last_slice); // $AUTOCOMPLETEIGNORESTATIC$
  writeprotected int GraphicFirstSlice, GraphicLastSlice, SliceRange, SliceWidth, SliceHeight;
  writeprotected float X, Y, Z;
};

struct SlicedWorld{
  import void AddSlicedObject(int x, int y, int z, int graphic_first_slice, int graphic_last_slice);
  import void Render(float angle_radians = 0.0);
  protected SlicedObject* SlicedObjects[MAX_SLICED_OBJECTS];
  protected int ObjectCount;
  protected Overlay* Overlays[MAX_OVERLAYS];
};

Sliced.asc
Code: ags
// new module script
// -- SLICED OBJECT --

void SlicedObject::Set(float x, float y, float z, int graphic_first_slice, int graphic_last_slice)
{
  this.X = x;
  this.Y = y;
  this.Z = z;
  this.GraphicFirstSlice = graphic_first_slice;
  this.GraphicLastSlice = graphic_last_slice;
  this.SliceRange = graphic_last_slice - graphic_first_slice;
  this.SliceWidth = Game.SpriteWidth[graphic_first_slice];
  this.SliceHeight = Game.SpriteHeight[graphic_first_slice];
}

static SlicedObject* SlicedObject::Create(float x, float y, float z, int graphic_first_slice, int graphic_last_slice)
{
  SlicedObject* sobj = new SlicedObject;
  sobj.Set(x, y, z, graphic_first_slice, graphic_last_slice);
  return sobj;
}

// -- SLICED WORLD --

void SlicedWorld::AddSlicedObject(int x, int y, int z, int graphic_first_slice, int graphic_last_slice)
{
  SlicedObject* sobj = SlicedObject.Create(IntToFloat(x), IntToFloat(y), IntToFloat(z), graphic_first_slice, graphic_last_slice);
  
  this.SlicedObjects[this.ObjectCount] = sobj;
  this.ObjectCount++;
}


void SlicedWorld::Render(float angle_radians)
{
  float pre_cos = Maths.Cos(angle_radians);
    float pre_sin = - Maths.Sin(angle_radians);
  
  for(int i=0; i<this.ObjectCount; i++)
  {
    SlicedObject* sobj = this.SlicedObjects[i];
    
    int overlay_id_start = i*OVERLAYS_STEP;
    int slice_range = sobj.SliceRange;
    
    for(int j=0; j<slice_range; j++) {
      int overlay_id = overlay_id_start + j;
      if(this.Overlays[overlay_id] == null || !this.Overlays[overlay_id].Valid) {
        this.Overlays[overlay_id] = Overlay.CreateRoomGraphical(0, 0, sobj.GraphicFirstSlice + j);
      }
      
      Overlay* ovr = this.Overlays[overlay_id];
      
      float dist = IntToFloat(j) + sobj.Z;
      
      float lx = dist * pre_cos;
      float ly = dist * pre_sin;
      
      ovr.X = FloatToInt(sobj.X - lx);
      ovr.Y = FloatToInt(sobj.Y - ly);
      ovr.Rotation = Maths.RadiansToDegrees(angle_radians);
    }
  }
}

Usage code

Code: ags
#define SPR_BARREL 1
#define SPR_BARREL_LAST 8
#define SPR_CHAIR 9
#define SPR_CHAIR_LAST 19
#define SPR_CHEST 20
#define SPR_CHEST_LAST 30

SlicedWorld sWorld;

function room_AfterFadeIn()
{

}

function room_Load()
{
  sWorld.AddSlicedObject(32, 32, 0, SPR_BARREL, SPR_BARREL_LAST);
  sWorld.AddSlicedObject(64, 32, 0, SPR_BARREL, SPR_BARREL_LAST);
  sWorld.AddSlicedObject(64, 96, 0, SPR_CHAIR, SPR_CHAIR_LAST);
  sWorld.AddSlicedObject(128, 96, 0, SPR_CHAIR, SPR_CHAIR_LAST);
  sWorld.AddSlicedObject(200, 48, 0, SPR_CHEST, SPR_CHEST_LAST);
  sWorld.AddSlicedObject(230, 48, 0, SPR_CHEST, SPR_CHEST_LAST);
  sWorld.AddSlicedObject(160, 96, 0, SPR_CHEST, SPR_CHEST_LAST);
  sWorld.AddSlicedObject(32, 111, 0, SPR_CHEST, SPR_CHEST_LAST);
}

float angl;
function room_RepExec()
{
  angl += 0.0125;
  sWorld.Render(angl);
}

So far my code and usage is like so, but I am not sure on this design. Any ideas?

(link to above project zip file to download)
#375
Hey, two minor things

  • in the first example, it should be SayBackground, with the S upper case
  • in the module code, the alignment centre is now center in 3.6.0, so there's I think three lines that need to be updated in the code - I think it's the renderer code

Used the module for the first time, it's really good!
#376
Don't Give Up the Cat

Download dont-give-up-the-cat.zip for Windows | dont-give-up-the-cat-linux.tar.gz for Linux

Tonight, you are out in the woods. As a cat.





This is my MAGS entry for this month! I plan to do a write-up, release the code and all that soon. The game is super short, as I did not have as much free time as I would like. I recommend headphones when playing it.
#377
Hi @Alan v.Drake , unfortunately I did not manage to reproduce it yet. I tried a bunch of ideas and it did not happen again, I was hoping to figure how to reproduce before opening an issue.  :(
#378
I think something like player.ChangeRoom(2, 32 /*x*/, player.y) should work, so you keep your y reference.

This way you fix the X position but makes the destination y position depend on the previous player y position.
#379
Hey, I have no idea yet what it is, but today I cut a piece of code and then when I hit save, before pasting it back, I got the error below. I don't know yet what it means. Using the 3.6.0.37 version.

Code: ags
Error: InvalidArgument=Value of '21' is not valid for 'index'.
Parameter name: index
Version: AGS 3.6.0.37

System.ArgumentOutOfRangeException: InvalidArgument=Value of '21' is not valid for 'index'.
Parameter name: index
   at System.Windows.Forms.ComboBox.ObjectCollection.get_Item(Int32 index)
   at AGS.Editor.ComboBoxCustom.<OnCreateControl>b__4_2(Object s, DrawItemEventArgs a)
   at System.Windows.Forms.DrawItemEventHandler.Invoke(Object sender, DrawItemEventArgs e)
   at System.Windows.Forms.ComboBox.OnDrawItem(DrawItemEventArgs e)
   at System.Windows.Forms.ComboBox.WmReflectDrawItem(Message& m)
   at System.Windows.Forms.ComboBox.WndProc(Message& m)
   at AGS.Editor.ComboBoxCustom.WndProc(Message& m)
   at System.Windows.Forms.Control.ControlNativeWindow.OnMessage(Message& m)
   at System.Windows.Forms.Control.ControlNativeWindow.WndProc(Message& m)
   at System.Windows.Forms.NativeWindow.Callback(IntPtr hWnd, Int32 msg, IntPtr wparam, IntPtr lparam)
#380
I don't know what normal font means, but if it's any font that comes with the templates - which are different fonts for each template -, I am not sure they have accents, but my memory tells they don't include any.
SMF spam blocked by CleanTalk