Advertisement
yclee126

Hexagonal grid ball bounce sim (Processing 3)

Jan 8th, 2023
859
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Java 14.36 KB | None | 0 0
  1. // Ball bouncing in a hexagonal grid
  2. // "pixel" unit bouncing simulator
  3. // Code by yclee126
  4. //
  5. // (The controls are also displayed in the HUD)
  6. //
  7. // Mouse:
  8. // Left drag: Draw walls
  9. // Right drag: Erase walls
  10. // Middle drag: Place a ball
  11. // Middle click: Delete a ball
  12. // Wheel: Speed (higher=slower)
  13. //
  14. // Keyboard:
  15. // T: Clear trails
  16. // W: Clear walls
  17. // B: Clear balls
  18. // A: Clear all
  19. // SPACE: Pause/Play
  20. // S: Step (hold it to "accelerate" in play mode)
  21. // C: Ball-ball collision
  22.  
  23. // Code for Processing 3
  24.  
  25. // Hex pattern in array
  26. /*
  27.  
  28. x
  29. 0 2 4 6
  30.  1 3 5 7
  31. 0 2 4 6
  32.  1 3 5 7
  33.  
  34. y
  35. 0 0 0 0
  36.  0 0 0 0
  37. 1 1 1 1
  38.  1 1 1 1
  39.  
  40. The point is that it closely resembles the rectangular array AND it stays connected no matter what the size is.
  41.  
  42. */
  43.  
  44. // ball collision rules:
  45. // 1. To change the direction, it must be blocked in the path. (can pass through narrow alleys)
  46. // 2. When it's blocked in the path, count each connected blocks in the cw and ccw direction, and bias the reflected path by its sum.
  47. // well, this is quite complicated, so I just used a lookup table in the code.
  48.  
  49. // Hexagonal directions are counted from 12'o clock in the cw direction.
  50. /*
  51.     0
  52.  5     1
  53.  
  54.  4     2
  55.     3
  56. */
  57.  
  58.  
  59. // global parameters
  60. // screen size is determined by hex_r and xCount, yCount -- be careful when putting large values
  61. final float hex_r = 40; // hexagon radius
  62. final float hex_h = hex_r*sqrt(3); // hexagon height (flat side down)
  63. final int xCount = 19;
  64. final int yCount = 10;
  65. final boolean halfY = false; // remove half of hexagons from the bottom row
  66. final boolean flipField = false; // flip xy
  67.  
  68. final int screenOffset = 30;
  69. final HashMap<String, Integer> collisionMap = new HashMap<String, Integer>(); // key: unwrapped wall in cw direction, value: delta direction
  70. final boolean DEBUG = false;
  71.  
  72. final int TRAIL = 1;
  73. final int WALL = 2;
  74. final int BALL = 4;
  75.  
  76.  
  77. // global variables
  78. final byte[][] hexTiles = new byte[yCount][xCount];
  79. final ArrayList<HexBall> hexBalls = new ArrayList<HexBall>();
  80.  
  81. int ballSpeed = 6;
  82. boolean ballCollision = true;
  83.  
  84. boolean paused = false;
  85. boolean updateStep = false;
  86. int steps = 0;
  87.  
  88. Point initMouseTile;
  89. boolean deleteBall = false;
  90. boolean addNew = false;
  91. int lastNewBallDir = 0;
  92.  
  93.  
  94. class Point {
  95.   int x, y;
  96.  
  97.   Point(int _x, int _y) {
  98.     x = _x;
  99.     y = _y;
  100.   }
  101. }
  102.  
  103. class PointF {
  104.   float x, y;
  105.  
  106.   PointF(float _x, float _y) {
  107.     x = _x;
  108.     y = _y;
  109.   }
  110. }
  111.  
  112. class HexBall {
  113.   int x, y, dir;
  114.  
  115.   HexBall(int _x, int _y, int _dir) {
  116.     x = _x;
  117.     y = _y;
  118.     dir = _dir;
  119.   }
  120.  
  121.   void update() {
  122.     // keep going to current direction
  123.     Point nextTile = getTileTo(x, y, dir);
  124.    
  125.     // check if collided
  126.     if(isWall(nextTile)) {
  127.       // "unwrap" surrounding 5 tiles in cw direction and find it on the list
  128.       String wall = "";
  129.       for(int i = -2; i <= 2; i ++) {
  130.         String result = isWall(x, y, applyDeltaDir(dir, i)) ? "1" : "0";
  131.         wall += result;
  132.       }
  133.      
  134.       int deltaDir = collisionMap.get(wall);
  135.       int finalDir = applyDeltaDir(dir, deltaDir);
  136.       nextTile = getTileTo(x, y, finalDir);
  137.      
  138.       // before actually moving check if there's a wall -- in this case update dir only
  139.       if(!isWall(nextTile)){
  140.         x = nextTile.x;
  141.         y = nextTile.y;
  142.       }
  143.       dir = finalDir;
  144.     }
  145.    
  146.     // keep going to this direction
  147.     else {
  148.       x = nextTile.x;
  149.       y = nextTile.y;
  150.     }
  151.   }
  152.  
  153.   void draw() {
  154.     pushStyle();
  155.    
  156.     // draw hex tile
  157.     fill(255, 0, 0, 128);
  158.     drawHexTile(x, y);
  159.    
  160.     // draw arrow indicating current direction
  161.     fill(0, 0, 0, 100);
  162.     stroke(0, 0);
  163.     drawArrowOnTile(x, y, dir*60);
  164.    
  165.     popStyle();
  166.   }
  167. }
  168.  
  169.  
  170. void settings() {
  171.   // adjust to hex count and size
  172.   int screenX = (int)(2*screenOffset+2*hex_r+3f/2*hex_r*(xCount-1));
  173.   int screenY = (int)(2*screenOffset+3f/2*hex_h+hex_h*(yCount-1) - (halfY ? hex_h/2 : 0));
  174.  
  175.   if(flipField) {
  176.      int temp = screenX;
  177.      screenX = screenY;
  178.      screenY = temp;
  179.   }
  180.  
  181.   size(screenX, screenY);
  182. }
  183.  
  184. void setup() {
  185.   // init map (why there's no easy static way, Java?)
  186.   // key: unwrapped wall in cw direction, value: delta direction
  187.   collisionMap.put("00100", 3);
  188.   collisionMap.put("01110", 3);
  189.   collisionMap.put("11111", 3);
  190.   collisionMap.put("00101", 3);
  191.   collisionMap.put("10100", 3);
  192.   collisionMap.put("01101", 3);
  193.   collisionMap.put("10110", 3);
  194.   collisionMap.put("10101", 3);
  195.  
  196.   collisionMap.put("01100", 2);
  197.   collisionMap.put("00110", -2);
  198.   collisionMap.put("11110", 2);
  199.   collisionMap.put("01111", -2);
  200.  
  201.   collisionMap.put("11100", 1);
  202.   collisionMap.put("00111", -1);
  203.   collisionMap.put("11101", 1);
  204.   collisionMap.put("10111", -1);
  205.  
  206.   // add a ball
  207.   // hexBalls.add(new HexBall(0, 9, 1));
  208. }
  209.  
  210. public void draw() {
  211.   background(255);
  212.  
  213.   // draw controls
  214.   fill(128);
  215.   textSize(screenOffset/2);
  216.  
  217.   textAlign(LEFT, TOP);
  218.   text("Mouse: L/R: draw/erase walls, M: add/remove balls, ↕: speed (" + ballSpeed + ")", 0, 0);
  219.  
  220.   textAlign(LEFT, BOTTOM);
  221.   text("Keys: clear Trails/Walls/Balls/All | SPACE(" + (paused ? "||" : ">") + ")/Step(" + steps + ") | Collision(" + (ballCollision ? "o" : "x") + ")", 0, height);
  222.  
  223.  
  224.   //add offset
  225.   translate(screenOffset, screenOffset);
  226.  
  227.   // flip xy
  228.   if (flipField) {
  229.     scale(-1, 1);
  230.     rotate(radians(90));
  231.   }
  232.  
  233.   // draw tile background
  234.   fill(255);
  235.   strokeWeight(hex_h/40);
  236.   for(int y = 0; y < yCount; y ++){
  237.     for(int x = 0; x < xCount; x ++){
  238.      
  239.       if(!tileInBound(x, y)){
  240.         continue;  
  241.       }
  242.       hexTiles[y][x] &= ~BALL; // remove prev "ball wall"s
  243.      
  244.       // render walls and trails
  245.       if(isWall(x, y)){
  246.         fill(140);
  247.       } else if((hexTiles[y][x] & TRAIL) > 0){
  248.         fill(255, 210, 180);
  249.       } else {
  250.         fill(255);
  251.       }
  252.      
  253.       drawHexTile(x, y);
  254.     }
  255.   }
  256.  
  257.   // get mouse pos and highlight it
  258.   Point tilePos = getTilePos(getMouseX(), getMouseY());
  259.   if (tilePos != null) {
  260.     fill(50, 128);
  261.     drawHexTile(tilePos.x, tilePos.y, !flipField);
  262.   }
  263.  
  264.   // draw hexballs
  265.   fill(255, 128, 0);
  266.   for(int i = 0; i < hexBalls.size(); i ++) {
  267.     HexBall ball = hexBalls.get(i);
  268.    
  269.     // update position
  270.     if(!paused && frameCount % ballSpeed == 0 || updateStep) {
  271.       ball.update();
  272.     }
  273.    
  274.     // draw ball and apply to the wall
  275.     ball.draw();
  276.     hexTiles[ball.y][ball.x] |= TRAIL;
  277.     if(ballCollision) {
  278.       hexTiles[ball.y][ball.x] |= BALL;
  279.     }
  280.   }
  281.  
  282.   // handle step
  283.   if (updateStep) {
  284.     steps += 1;
  285.   } else if(!paused) {
  286.     steps = 0;
  287.   }
  288.   updateStep = false; // always clear the steps variable, even the balls are not there
  289. }
  290.  
  291. boolean tileInBound(int x, int y) {
  292.   if(halfY && y == yCount-1 && x%2 == 1){
  293.     return false;
  294.   }
  295.   return x >= 0 && x < xCount && y >= 0 && y < yCount;
  296. }
  297.  
  298. Point getTilePos(int x, int y) {
  299.   // accurate hex tile positioning
  300.  
  301.   // rectangular pos
  302.   int xPos = (int)(x / (3f/2*hex_r));
  303.   int yPos = (int)((y - (xPos%2 == 0 ? 0 : hex_h/2)) / hex_h);
  304.  
  305.   Point pos = new Point(xPos, yPos);
  306.  
  307.   // check if xpos lies in the vertical zig-zag zone and correct it
  308.   // that is, the two small trialgles notated 4 and 5
  309.   //
  310.   // |   |       |
  311.   // |555#########
  312.   // |55#|       |#
  313.   // |5# |       | #
  314.   // |#  |       |  #
  315.   // #   |       |   #
  316.   // |#  |       |  #
  317.   // |4# |       | #
  318.   // |44#|       |#
  319.   // |444#########
  320.   // |   |       |
  321.  
  322.   // modulo'd values
  323.   float xm = x%(3f/2*hex_r);
  324.   float ym = (y - (xPos%2 == 0 ? 0 : hex_h/2)) % hex_h;
  325.  
  326.   if (xm < hex_r/2*abs(ym/(hex_h/2)-1)) {
  327.     if (ym < hex_h/2) {
  328.       pos = getTileTo(xPos, yPos, 5);      
  329.     } else {
  330.       pos = getTileTo(xPos, yPos, 4);
  331.     }
  332.   }
  333.  
  334.   // check if out of bounds
  335.   if (!tileInBound(pos.x, pos.y)) {
  336.      pos = null;
  337.   }
  338.  
  339.   // no need for correction, return the pos
  340.   return pos;
  341. }
  342.  
  343. PointF getTileCoord(int x, int y) {
  344.   // calc coords
  345.   float xCoord = hex_r + 3f/2*hex_r*x;
  346.   float yCoord = hex_h/2 + (x%2 == 1 ? hex_h/2 : 0) + hex_h*y;
  347.  
  348.   return new PointF(xCoord, yCoord);
  349. }
  350.  
  351. void drawHexTile(int x, int y) {
  352.   drawHexTile(x, y, false);  
  353. }
  354.  
  355. void drawHexTile(int x, int y, boolean drawNumbers) {
  356.   // calc coords
  357.   PointF point = getTileCoord(x, y);
  358.  
  359.   // draw polygon
  360.   polygon(point.x, point.y, hex_r, 6);
  361.  
  362.   // draw text (debug)
  363.   if(drawNumbers || DEBUG) {
  364.     pushStyle();
  365.    
  366.     fill(0);
  367.     textSize(20);
  368.     textAlign(CENTER, CENTER);
  369.     text(String.format("%d, %d", x, y), point.x, point.y);
  370.    
  371.     popStyle();
  372.   }
  373. }
  374.  
  375. Point getTileTo(int x, int y, int dir) {
  376.   // "I'm here, what's the next tile to this direction?"
  377.   // wrapping is allowed
  378.  
  379.   int newX = 0, newY = 0;
  380.  
  381.   // assume it's heading to direction 0
  382.   switch(dir) {
  383.     case 0:
  384.     newX = x;
  385.     newY = y - 1;
  386.     break;
  387.    
  388.     case 1:
  389.     newX = x + 1;
  390.     newY = y - (x%2 == 0 ? 1 : 0);
  391.     break;
  392.    
  393.     case 2:
  394.     newX = x + 1;
  395.     newY = y + (x%2 == 1 ? 1 : 0);
  396.     break;
  397.    
  398.     case 3:
  399.     newX = x;
  400.     newY = y + 1;
  401.     break;
  402.    
  403.     case 4:
  404.     newX = x - 1;
  405.     newY = y + (x%2 == 1 ? 1 : 0);
  406.     break;
  407.    
  408.     case 5:
  409.     newX = x - 1;
  410.     newY = y - (x%2 == 0 ? 1 : 0);
  411.     break;
  412.   }
  413.  
  414.   return new Point(newX, newY);
  415. }
  416.  
  417. void drawArrowOnTile(int x, int y, float a) {
  418.   pushMatrix();
  419.  
  420.   // move to position
  421.   PointF p = getTileCoord(x, y);
  422.   translate(p.x, p.y);
  423.   rotate(radians(a));
  424.  
  425.   // draw arrow
  426.   beginShape();
  427.  
  428.   // arrow head
  429.   vertex(-hex_r*0.5f, 0);
  430.   vertex(0, -hex_r*0.6f);
  431.   vertex(hex_r*0.5f, 0);
  432.  
  433.   // arrow body
  434.   vertex(hex_r*0.2f, 0);
  435.   vertex(hex_r*0.2f, hex_r*0.6f);
  436.   vertex(-hex_r*0.2f, hex_r*0.6f);
  437.   vertex(-hex_r*0.2f, 0);
  438.  
  439.   endShape(CLOSE);
  440.  
  441.   popMatrix();
  442. }
  443.  
  444. void polygon(float x, float y, float radius, int npoints) {
  445.   float angle = TWO_PI / npoints;
  446.   beginShape();
  447.   for (float a = 0; a < TWO_PI; a += angle) {
  448.     float sx = x + cos(a) * radius;
  449.     float sy = y + sin(a) * radius;
  450.     vertex(sx, sy);
  451.   }
  452.   endShape(CLOSE);
  453. }
  454.  
  455. boolean isWall(int x, int y, int dir) {
  456.   Point point = getTileTo(x, y, dir);
  457.   return isWall(point);
  458. }
  459.  
  460. boolean isWall(Point p) {
  461.   return isWall(p.x, p.y);
  462. }
  463.  
  464. boolean isWall(int x, int y) {
  465.   // out of bounds check
  466.   if (!tileInBound(x, y)) {
  467.     return true;
  468.   }
  469.  
  470.   // actual array check
  471.   return hexTiles[y][x] >= WALL;
  472. }
  473.  
  474.  
  475. int applyDeltaDir(int currDir, int deltaDir) {
  476.   int finalDir = (currDir + deltaDir) % 6;
  477.   finalDir += finalDir < 0 ? 6 : 0;
  478.   return finalDir;
  479. }
  480.  
  481.  
  482. void mousePressed() {
  483.   initMouseTile = getTilePos(getMouseX(), getMouseY());
  484.   addNew = false;
  485.   deleteBall = false;
  486.   mouseCallback();
  487. }
  488.  
  489. void mouseReleased() {
  490.   if(initMouseTile != null){
  491.     if(addNew){
  492.       HexBall newBall = new HexBall(initMouseTile.x, initMouseTile.y, lastNewBallDir);
  493.       hexBalls.add(newBall);
  494.       hexTiles[newBall.y][newBall.x] |= TRAIL;
  495.     }
  496.     else if(deleteBall){
  497.       for(int i = 0; i < hexBalls.size(); i ++){
  498.         HexBall ball = hexBalls.get(i);
  499.         if (ball.x == initMouseTile.x && ball.y == initMouseTile.y) {
  500.           hexBalls.remove(i);
  501.           i --;
  502.         }
  503.       }
  504.     }
  505.   }
  506. }
  507.  
  508. void mouseDragged() {
  509.   mouseCallback();
  510. }
  511.  
  512. void keyPressed() {
  513.   switch(key) {
  514.      case 'a':
  515.       hexBalls.clear();
  516.       for(int y = 0; y < yCount; y ++){
  517.         for(int x = 0; x < xCount; x ++){
  518.           hexTiles[y][x] = 0;
  519.         }
  520.       }
  521.       break;
  522.      
  523.      case 'c':
  524.       ballCollision = !ballCollision;
  525.       break;
  526.      
  527.      case 't':
  528.       for(int y = 0; y < yCount; y ++){
  529.         for(int x = 0; x < xCount; x ++){
  530.           hexTiles[y][x] &= ~TRAIL;
  531.         }
  532.       }
  533.       break;
  534.      
  535.      case 'b':
  536.       hexBalls.clear();
  537.       break;
  538.      
  539.      case 'w':
  540.       for(int y = 0; y < yCount; y ++){
  541.         for(int x = 0; x < xCount; x ++){
  542.           hexTiles[y][x] &= ~WALL;
  543.         }
  544.       }
  545.       break;
  546.      
  547.      case ' ':
  548.       paused = !paused;
  549.       break;
  550.      
  551.      case 's':
  552.       updateStep = true;
  553.       break;
  554.   }
  555. }
  556.  
  557. int getMouseX() {
  558.   return (flipField ? mouseY : mouseX) - screenOffset;
  559. }
  560.  
  561. int getMouseY() {
  562.   return (flipField ? mouseX : mouseY) - screenOffset;
  563. }
  564.  
  565. void mouseCallback() {
  566.   Point tilePos = getTilePos(getMouseX(), getMouseY());
  567.  
  568.   // draw walls
  569.   if(mouseButton == LEFT && tilePos != null) {
  570.     hexTiles[max(min(tilePos.y, yCount-1), 0)][max(min(tilePos.x, xCount-1), 0)] |= WALL;    
  571.   }
  572.  
  573.   // erase walls
  574.   else if(mouseButton == RIGHT && tilePos != null) {
  575.     hexTiles[max(min(tilePos.y, yCount-1), 0)][max(min(tilePos.x, xCount-1), 0)] &= ~WALL;
  576.   }
  577.  
  578.   // delete (on the same tile) / place a ball
  579.   else if(mouseButton == CENTER && initMouseTile != null) {
  580.     // delete if possible
  581.     if(tilePos != null && tilePos.x == initMouseTile.x && tilePos.y == initMouseTile.y) {
  582.        addNew = false;
  583.        deleteBall = true;
  584.      
  585.        // draw a cross
  586.       pushStyle();
  587.       pushMatrix();
  588.      
  589.       fill(0, 0, 0);
  590.       stroke(0, 0);
  591.       rectMode(CENTER);
  592.      
  593.       PointF pos = getTileCoord(initMouseTile.x, initMouseTile.y);
  594.      
  595.       translate(pos.x, pos.y);
  596.       rotate(radians(45));
  597.       rect(0, 0, hex_r*1.2, hex_r*0.2);
  598.       rotate(radians(90));
  599.       rect(0, 0, hex_r*1.2, hex_r*0.2);
  600.      
  601.       popMatrix();
  602.       popStyle();
  603.     }
  604.    
  605.     // add a new one
  606.     else {
  607.       addNew = true;
  608.       deleteBall = false;
  609.      
  610.       // get delta from the center of the tile
  611.       PointF initPoint = getTileCoord(initMouseTile.x, initMouseTile.y);
  612.       float delta_x = getMouseX() - initPoint.x;
  613.       float delta_y = -(getMouseY() - initPoint.y);
  614.       float angle = degrees(atan2(delta_y, delta_x));
  615.      
  616.       // clockwise from 12'o clock
  617.       angle = (-angle+180-90+360) % 360;
  618.      
  619.       // add 30deg offset to center the ranges
  620.       angle = (angle+30) % 360;
  621.      
  622.       // save the direction
  623.       lastNewBallDir = (int)(angle / 60);
  624.      
  625.       // display the angle
  626.       pushStyle();
  627.       fill(0, 0, 0);
  628.       stroke(0, 0);
  629.       drawArrowOnTile(initMouseTile.x, initMouseTile.y, lastNewBallDir*60);
  630.       popStyle();
  631.     }
  632.   }
  633. }
  634.  
  635. void mouseWheel(MouseEvent event) {
  636.   if (event.getCount() > 0) {
  637.     ballSpeed = min(ballSpeed+1, 60);
  638.   }
  639.   else {
  640.     ballSpeed = max(ballSpeed-1, 1);
  641.   }
  642. }
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement