I'm trying to make a game and I'm stuck on random level design. Basically, I'm trying to create a line from one edge/corner to another edge/corner while having some randomness to it.
See below image 1 [link broken] and 2 for examples. I'm doing this in processing and every attempt I've tried hasn't yielded proper results. I can get them to populate randomly but not in a line or from edge to edge. I'm trying to do this on a 16 x 16 grid by the way. Any ideas or help would be greatly appreciated thanks!
Image 2:
Based on your description, the challenge is in having a connected line from top to bottom with a bit of randomness driving left/right direction.
There are multiple options.
Here's a basic idea that comes to mind:
pick a starting x position: left's say right down the middle
for each row from 0 to 15 (for 16 px level)
pick a random between 3 numbers:
if it's the 1st go left (x decrements)
if it's the 2nd go right (x increments)
if it's the 3rd: ignore: it means the line will go straight down for this iteration
Here's a basic sketch that illustrates this using PImage to visualise the data:
void setup(){
size(160, 160);
noSmooth();
int levelSize = 16;
PImage level = createImage(levelSize, levelSize, RGB);
level.loadPixels();
java.util.Arrays.fill(level.pixels, color(255));
int x = levelSize / 2;
for(int y = 0 ; y < levelSize; y++){
int randomDirection = (int)random(3);
if(randomDirection == 1) x--;
if(randomDirection == 2) x++;
// if randomDirection is 0 ignore as we don't change x -> just go down
// constrain to valid pixel
x = constrain(x, 0, levelSize - 1);
// render dot
level.pixels[x + y * levelSize] = color(0);
}
level.updatePixels();
// render result;
image(level, 0, 0, width, height);
fill(127);
text("click to reset", 10, 15);
}
// hacky reset
void draw(){}
void mousePressed(){
setup();
}
The logic is be pretty plain above, but free to replace random(3) with other options (perhaps throwing dice to determine direction or exploring other psuedo-random number generators (PRNGs) such as randomGaussian(), noise() (and related functions), etc.)
Here's a p5.js version of the above:
let levelSize = 16;
let numBlocks = levelSize * levelSize;
let level = new Array(numBlocks);
function setup() {
createCanvas(320, 320);
level.fill(0);
let x = floor(levelSize / 2);
for(let y = 0 ; y < levelSize; y++){
let randomDirection = floor(random(3));
if(randomDirection === 1) x--;
if(randomDirection === 2) x++;
// if randomDirection is 0 ignore as we don't change x -> just go down
// constrain to valid pixel
x = constrain(x, 0, levelSize - 1);
// render dot
level[x + y * levelSize] = 1;
}
// optional: print to console
// prettyPrintLevel(level, levelSize, numBlocks);
}
function draw() {
background(255);
// visualise
for(let i = 0 ; i < numBlocks; i++){
let x = i % levelSize;
let y = floor(i / levelSize);
fill(level[i] == 1 ? color(0) : color(255));
rect(x * 20, y * 20, 20, 20);
}
}
function prettyPrintLevel(level, levelSize, numBlocks){
for(let i = 0; i < numBlocks; i+= levelSize){
print(level.slice(i, i + levelSize));
}
}
function mousePressed(){
setup();
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.4.1/p5.min.js"></script>
The data is a structured a 1D array in both examples, however, if it makes it easier it could easily be a 2D array. At this stage of development, whatever is the simplest, most readable option is the way to go.
i have a task to make a pattern of circles and squares as described on photo, and i need to animate it so that all objects smoothly increase to four times the size and then shrink back to their original size and this is repeated. i tried but i cant understand problem
{
size(500,500);
background(#A5A3A3);
noFill();
rectMode(CENTER);
ellipseMode(CENTER);
}
void pattern(int a, int b)
{
boolean isShrinking = false;
for(int x = 0; x <= width; x += a){
for(int y = 0; y <= height; y += a){
stroke(#1B08FF);
ellipse(x,y,a,a);
stroke(#FF0000);
rect(x,y,a,a);
stroke(#0BFF00);
ellipse(x+25,y+25,a/2,a/2);
if (isShrinking){a -= b;}
else {a += b;}
if (a == 50 || a == 200){
isShrinking = !isShrinking ; }
}
}
}
void draw()
{
pattern(50,1);
}
this is what pattern need to look like
Great that you've posted your attempt.
From what you presented I can't understand the problem either. If this is an assignment, perhaps try to get more clarifications ?
If you comment you the isShrinking part of the code indeed you have an drawing similar to image you posted.
animate it so that all objects smoothly increase to four times the size and then shrink back to their original size and this is repeated
Does that simply mean scaling the whole pattern ?
If so, you can make use of the sine function (sin()) and the map() function to achieve that:
sin(), as the reference mentions, returns a value between -1 and 1 when you pass it an angle between 0 and 2 * PI (because in Processing trig. functions use radians not degrees for angles)
You can use frameCount divided by a fractional value to mimic an even increasing angle. (Even if you go around the circle multiple times (angle > 2 * PI), sin() will still return a value between -1 and 1)
map() takes a single value from one number range and maps it to another. (In your case from sin()'s result (-1,1) to the scale range (1,4)
Here's a tweaked version of your code with the above notes:
void setup()
{
size(500, 500, FX2D);
background(#A5A3A3);
noFill();
rectMode(CENTER);
ellipseMode(CENTER);
}
void pattern(int a)
{
for (int x = 0; x <= width; x += a) {
for (int y = 0; y <= height; y += a) {
stroke(#1B08FF);
ellipse(x, y, a, a);
stroke(#FF0000);
rect(x, y, a, a);
stroke(#0BFF00);
ellipse(x+25, y+25, a/2, a/2);
}
}
}
void draw()
{
// clear frame (previous drawings)
background(255);
// use the frame number as if it's an angle
float angleInRadians = frameCount * .01;
// map the sin of the frame based angle to the scale range
float sinAsScale = map(sin(angleInRadians), -1, 1, 1, 4);
// apply the scale
scale(sinAsScale);
// render the pattern (at current scale)
pattern(50);
}
(I've chosen the FX2D renderer because it's smoother in this case.
Additionally I advise in the future formatting the code. It makes it so much easier to read and it barely takes any effort (press Ctrl+T). On the long run you'll read code more than you'll write it, especially on large programs and heaving code that's easy to read will save you plenty of time and potentially headaches.)
I need to loop through a array in circle in arc shape with a small radius (like draw a circle pixel by pixel), but all algorithm i tried, checks duplicate indexes of array (it's got the same x and y several times).
I have a radius of 3, with a circle form of 28 elements (not filled), but the algorithm iterate 360 times. I can check if x or y change before i do something, but it's lame.
My code now:
for (int radius = 1; radius < 6; radius++)
{
for (double i = 0; i < 360; i += 1)
{
double angle = i * System.Math.PI / 180;
int x = (int)(radius * System.Math.Cos(angle)) + centerX;
int y = (int)(radius * System.Math.Sin(angle)) + centerY;
// do something
// if (array[x, y]) ....
}
}
PS: I can't use midpoint circle, because i need to increment radius starting from 2 until 6, and not every index is obtained, because his circle it's not real (according trigonometry)
EDIT:
What i really need, is scan a full circle edge by edge, starting by center.
360 steps (it's get all coordinates):
Full scan
for (int radius = 2; radius <= 7; radius++)
{
for (double i = 0; i <= 360; i += 1)
{
double angle = i * System.Math.PI / 180;
int x = (int)(radius * System.Math.Cos(angle));
int y = (int)(radius * System.Math.Sin(angle));
print(x, y, "X");
}
}
Using Midpoint Circle or other algorithm skipping steps (missing coordinates):
Midpoint Circle Algorithm
for (int radius = 2; radius <= 7; radius++)
{
int x = radius;
int y = 0;
int err = 0;
while (x >= y)
{
print(x, y, "X");
print(y, x, "X");
print(-y, x, "X");
print(-y, x, "X");
print(-x, y, "X");
print(-x, -y, "X");
print(-y, -x, "X");
print(y, -x, "X");
print(x, -y, "X");
y += 1;
err += 1 + 2 * y;
if (2 * (err - x) + 1 > 0)
{
x -= 1;
err += 1 - 2 * x;
}
}
}
There are two algorithmic ideas in play here: one is rasterizing a circle. The OP code presents a couple opportunities for improvement on that front: (a) one needn't sample the entire 360 degree circle, realizing that a circle is symmetric across both axes. (x,y) can be reflected in the other three quadrants as (-x,y), (-x,-y), and (x,-y). (b) the step on the loop should be related to the curvature. A simple heuristic is to use the radius as the step. So...
let step = MIN(radius, 90)
for (double i=0; i<90; i += step) {
add (x,y) to results
reflect into quadrants 2,3,4 and add to results
}
With these couple improvements, you may no longer care about duplicate samples being generated. If you still do, then the second idea, independent of the circle, is how to hash a pair of ints. There's a good article about that here: Mapping two integers to one, in a unique and deterministic way.
In a nutshell, we compute an int from our x,y pair that's guaranteed to map uniquely, and then check that for duplicates...
cantor(x, y) = 1/2(x + y)(x + y + 1) + y
This works only for positive values of x,y, which is just what you need since we're only computing (and then reflecting) in the first quadrant. For each pair, check that they are unique
let s = an empty set
int step = MIN(radius, 90)
for (double i=0; i<90; i += step) {
generate (x,y)
let c = cantor(x,y)
if (not(s contains c)) {
add (x,y) to results
reflect into quadrants 2,3,4 and add to results
add c to s
}
}
Got it!
It's not beautiful, but work for me.
int maxRadius = 7;
for (int radius = 1; radius <= maxRadius; radius++)
{
x = position.X - radius;
y = position.Y - radius;
x2 = position.X + radius;
y2 = position.Y + radius;
for (int i = 0; i <= radius * 2; i++)
{
if (InCircle(position.X, position.Y, x + i, y, maxRadius)) // Top X
myArray[position, x + i, y]; // check array
if (InCircle(position.X, position.Y, x + i, y2, maxRadius)) // Bottom X
myArray[position, x + i, y2]; // check array
if (i > 0 && i < radius * 2)
{
if (InCircle(position.X, position.Y, x, y + i, maxRadius)) // Left Y
myArray[position, x, y + i]; // check array
if (InCircle(position.X, position.Y, x2, y + i, maxRadius)) // Right Y
myArray[position, x2, y + i]; // check array
}
}
}
public static bool InCircle(int originX, int originY, int x, int y, int radius)
{
int dx = Math.Abs(x - originX);
if (dx > radius) return false;
int dy = Math.Abs(y - originY);
if (dy > radius) return false;
if (dx + dy <= radius) return true;
return (dx * dx + dy * dy <= radius * radius);
}
I'm trying to draw bears in processing, (Just simple circles), how can I get the bears equally spaced apart, and have the same space from the edge of the screen to the bears, on either side? As well as vertically.
I know this is vague, but I'm terrible at explaining things
Because you does not provide any code or example I will just tell you how to place circle in the middle of sketch.
For simplicity imagine this set up:
void setup(){
size(400, 400);
}
1) Very basic approach would be to hard code position of this circle into ellipse draw function.
ellipse(200, 200, 50, 50);
Where first two parameters are coordinates for circle center. Simple find out from size 400x400 that mid is on coord 200x200. This is bad approach and you should avoid using it.
2) Better approach would be to calculate center coord using global variables width and height
ellipse(width/2, height/2, 50, 50);
3) When you are drawing or moving more complex objects it is preferred to use some function to draw this objects always with same fixed position in our example
void draw_circle(){
ellipse(0, 0, 50, 50);
}
And just moving center of drawing using transformations so our draw function will looks like this
void draw(){
pushMatrix();
translate(width/2, height/2);
draw_circle();
popMatrix();
}
Using this you could be able to draw bears equally spaced apart and from sides.
It sounds like you want a grid of equally spaced circles. For that you just need to divide your space into a grid in the x and y directions. The simplest way to do this is to wrap the kind of thing Majlik showed inside a double loop to move from cell to cell in your 'virtual' grid. To see this more clearly, in the code below there is an extra little bit so that if you press the 'g' key (for grid) you'll see the grid cells, with a circle centered in each one. You can press any other key to make the grid go away.
You can see that each way gives the same result: inside draw() uncomment the one you want and comment out the other 2.
int nx = 4; // number of circles horizontally
int ny = 5; // number of circles vertically
int divx;
int divy;
int diameter = 40;
void setup() {
size(600, 600);
// calculate width and hegith of each cell of the grid
divx = width/nx;
divy = height/ny;
}
// 3 ways to draw a regular grid of circles
void draw() {
background(200);
// show the cell layout if the g key was typed, otherwise don't
if(key == 'g')
drawGrid();
// 1 way
for(int i = 0; i < nx; i++) {
for(int j = 0; j < ny; j++ ) {
ellipse(i * divx + divx/2, j * divy + divy/2, diameter, diameter);
}
}
// another way
// for(int i = divx/2; i < width; i += divx) {
// for(int j = divy/2; j < height; j += divy ) {
// ellipse(i, j, diameter, diameter);
// }
// }
// yet another way
// for(int i = divx/2; i < width; i += divx) {
// for(int j = divy/2; j < height; j += divy ) {
// pushMatrix();
// translate(i, j);
// ellipse(0, 0, diameter, diameter);
// popMatrix();
// }
// }
}
void drawGrid() {
// draw vertical lines
for(int i = 1; i < nx; i++) {
line(i * divx, 0, i * divx, height);
}
// draw horizontal lines
for(int j = 1; j < ny; j++ ) {
line(0, j * divy, width, j * divy);
}
}
This problem actually deals with roll-overs, I'll just generalized below as such:
I have a 2D view, and I have a number of rectangles within an area on the screen. How do I spread out those boxes such that they don't overlap each other, but only adjust them with minimal moving?
The rectangles' positions are dynamic and dependent on user's input, so their positions could be anywhere.
Attached images show the problem and desired solution
The real life problem deals with rollovers, actually.
Answers to the questions in the comments
Size of rectangles is not fixed, and is dependent on the length of the text in the rollover
About screen size, right now I think it's better to assume that the size of the screen is enough for the rectangles. If there is too many rectangles and the algo produces no solution, then I just have to tweak the content.
The requirement to 'move minimally' is more for asethetics than an absolute engineering requirement. One could space out two rectangles by adding a vast distance between them, but it won't look good as part of the GUI. The idea is to get the rollover/rectangle as close as to its source (which I will then connect to the source with a black line). So either 'moving just one for x' or 'moving both for half x' is fine.
I was working a bit in this, as I also needed something similar, but I had delayed the algorithm development. You helped me to get some impulse :D
I also needed the source code, so here it is. I worked it out in Mathematica, but as I haven't used heavily the functional features, I guess it'll be easy to translate to any procedural language.
A historic perspective
First I decided to develop the algorithm for circles, because the intersection is easier to calculate. It just depends on the centers and radii.
I was able to use the Mathematica equation solver, and it performed nicely.
Just look:
It was easy. I just loaded the solver with the following problem:
For each circle
Solve[
Find new coördinates for the circle
Minimizing the distance to the geometric center of the image
Taking in account that
Distance between centers > R1+R2 *for all other circles
Move the circle in a line between its center and the
geometric center of the drawing
]
As straightforward as that, and Mathematica did all the work.
I said "Ha! it's easy, now let's go for the rectangles!". But I was wrong ...
Rectangular Blues
The main problem with the rectangles is that querying the intersection is a nasty function. Something like:
So, when I tried to feed up Mathematica with a lot of these conditions for the equation, it performed so badly that I decided to do something procedural.
My algorithm ended up as follows:
Expand each rectangle size by a few points to get gaps in final configuration
While There are intersections
sort list of rectangles by number of intersections
push most intersected rectangle on stack, and remove it from list
// Now all remaining rectangles doesn't intersect each other
While stack not empty
pop rectangle from stack and re-insert it into list
find the geometric center G of the chart (each time!)
find the movement vector M (from G to rectangle center)
move the rectangle incrementally in the direction of M (both sides)
until no intersections
Shrink the rectangles to its original size
You may note that the "smallest movement" condition is not completely satisfied (only in one direction). But I found that moving the rectangles in any direction to satisfy it, sometimes ends up with a confusing map changing for the user.
As I am designing a user interface, I choose to move the rectangle a little further, but in a more predictable way. You can change the algorithm to inspect all angles and all radii surrounding its current position until an empty place is found, although it'll be much more demanding.
Anyway, these are examples of the results (before/ after):
Edit> More examples here
As you may see, the "minimum movement" is not satisfied, but the results are good enough.
I'll post the code here because I'm having some trouble with my SVN repository. I'll remove it when the problems are solved.
Edit:
You may also use R-Trees for finding rectangle intersections, but it seems an overkill for dealing with a small number of rectangles. And I haven't the algorithms already implemented. Perhaps someone else can point you to an existing implementation on your platform of choice.
Warning! Code is a first approach .. not great quality yet, and surely has some bugs.
It's Mathematica.
(*Define some functions first*)
Clear["Global`*"];
rn[x_] := RandomReal[{0, x}];
rnR[x_] := RandomReal[{1, x}];
rndCol[] := RGBColor[rn[1], rn[1], rn[1]];
minX[l_, i_] := l[[i]][[1]][[1]]; (*just for easy reading*)
maxX[l_, i_] := l[[i]][[1]][[2]];
minY[l_, i_] := l[[i]][[2]][[1]];
maxY[l_, i_] := l[[i]][[2]][[2]];
color[l_, i_]:= l[[i]][[3]];
intersectsQ[l_, i_, j_] := (* l list, (i,j) indexes,
list={{x1,x2},{y1,y2}} *)
(*A rect does intesect with itself*)
If[Max[minX[l, i], minX[l, j]] < Min[maxX[l, i], maxX[l, j]] &&
Max[minY[l, i], minY[l, j]] < Min[maxY[l, i], maxY[l, j]],
True,False];
(* Number of Intersects for a Rectangle *)
(* With i as index*)
countIntersects[l_, i_] :=
Count[Table[intersectsQ[l, i, j], {j, 1, Length[l]}], True]-1;
(*And With r as rectangle *)
countIntersectsR[l_, r_] := (
Return[Count[Table[intersectsQ[Append[l, r], Length[l] + 1, j],
{j, 1, Length[l] + 1}], True] - 2];)
(* Get the maximum intersections for all rectangles*)
findMaxIntesections[l_] := Max[Table[countIntersects[l, i],
{i, 1, Length[l]}]];
(* Get the rectangle center *)
rectCenter[l_, i_] := {1/2 (maxX[l, i] + minX[l, i] ),
1/2 (maxY[l, i] + minY[l, i] )};
(* Get the Geom center of the whole figure (list), to move aesthetically*)
geometryCenter[l_] := (* returs {x,y} *)
Mean[Table[rectCenter[l, i], {i, Length[l]}]];
(* Increment or decr. size of all rects by a bit (put/remove borders)*)
changeSize[l_, incr_] :=
Table[{{minX[l, i] - incr, maxX[l, i] + incr},
{minY[l, i] - incr, maxY[l, i] + incr},
color[l, i]},
{i, Length[l]}];
sortListByIntersections[l_] := (* Order list by most intersecting Rects*)
Module[{a, b},
a = MapIndexed[{countIntersectsR[l, #1], #2} &, l];
b = SortBy[a, -#[[1]] &];
Return[Table[l[[b[[i]][[2]][[1]]]], {i, Length[b]}]];
];
(* Utility Functions*)
deb[x_] := (Print["--------"]; Print[x]; Print["---------"];)(* for debug *)
tableForPlot[l_] := (*for plotting*)
Table[{color[l, i], Rectangle[{minX[l, i], minY[l, i]},
{maxX[l, i], maxY[l, i]}]}, {i, Length[l]}];
genList[nonOverlap_, Overlap_] := (* Generate initial lists of rects*)
Module[{alist, blist, a, b},
(alist = (* Generate non overlapping - Tabuloid *)
Table[{{Mod[i, 3], Mod[i, 3] + .8},
{Mod[i, 4], Mod[i, 4] + .8},
rndCol[]}, {i, nonOverlap}];
blist = (* Random overlapping *)
Table[{{a = rnR[3], a + rnR[2]}, {b = rnR[3], b + rnR[2]},
rndCol[]}, {Overlap}];
Return[Join[alist, blist] (* Join both *)];)
];
Main
clist = genList[6, 4]; (* Generate a mix fixed & random set *)
incr = 0.05; (* may be some heuristics needed to determine best increment*)
clist = changeSize[clist,incr]; (* expand rects so that borders does not
touch each other*)
(* Now remove all intercepting rectangles until no more intersections *)
workList = {}; (* the stack*)
While[findMaxIntesections[clist] > 0,
(*Iterate until no intersections *)
clist = sortListByIntersections[clist];
(*Put the most intersected first*)
PrependTo[workList, First[clist]];
(* Push workList with intersected *)
clist = Delete[clist, 1]; (* and Drop it from clist *)
];
(* There are no intersections now, lets pop the stack*)
While [workList != {},
PrependTo[clist, First[workList]];
(*Push first element in front of clist*)
workList = Delete[workList, 1];
(* and Drop it from worklist *)
toMoveIndex = 1;
(*Will move the most intersected Rect*)
g = geometryCenter[clist];
(*so the geom. perception is preserved*)
vectorToMove = rectCenter[clist, toMoveIndex] - g;
If [Norm[vectorToMove] < 0.01, vectorToMove = {1,1}]; (*just in case*)
vectorToMove = vectorToMove/Norm[vectorToMove];
(*to manage step size wisely*)
(*Now iterate finding minimum move first one way, then the other*)
i = 1; (*movement quantity*)
While[countIntersects[clist, toMoveIndex] != 0,
(*If the Rect still intersects*)
(*move it alternating ways (-1)^n *)
clist[[toMoveIndex]][[1]] += (-1)^i i incr vectorToMove[[1]];(*X coords*)
clist[[toMoveIndex]][[2]] += (-1)^i i incr vectorToMove[[2]];(*Y coords*)
i++;
];
];
clist = changeSize[clist, -incr](* restore original sizes*);
HTH!
Edit: Multi-angle searching
I implemented a change in the algorithm allowing to search in all directions, but giving preference to the axis imposed by the geometric symmetry.
At the expense of more cycles, this resulted in more compact final configurations, as you can see here below:
More samples here.
The pseudocode for the main loop changed to:
Expand each rectangle size by a few points to get gaps in final configuration
While There are intersections
sort list of rectangles by number of intersections
push most intersected rectangle on stack, and remove it from list
// Now all remaining rectangles doesn't intersect each other
While stack not empty
find the geometric center G of the chart (each time!)
find the PREFERRED movement vector M (from G to rectangle center)
pop rectangle from stack
With the rectangle
While there are intersections (list+rectangle)
For increasing movement modulus
For increasing angle (0, Pi/4)
rotate vector M expanding the angle alongside M
(* angle, -angle, Pi + angle, Pi-angle*)
re-position the rectangle accorging to M
Re-insert modified vector into list
Shrink the rectangles to its original size
I'm not including the source code for brevity, but just ask for it if you think you can use it. I think that, should you go this way, it's better to switch to R-trees (a lot of interval tests needed here)
Here's a guess.
Find the center C of the bounding box of your rectangles.
For each rectangle R that overlaps another.
Define a movement vector v.
Find all the rectangles R' that overlap R.
Add a vector to v proportional to the vector between the center of R and R'.
Add a vector to v proportional to the vector between C and the center of R.
Move R by v.
Repeat until nothing overlaps.
This incrementally moves the rectangles away from each other and the center of all the rectangles. This will terminate because the component of v from step 4 will eventually spread them out enough all by itself.
I think this solution is quite similar to the one given by cape1232, but it's already implemented, so worth checking out :)
Follow to this reddit discussion: http://www.reddit.com/r/gamedev/comments/1dlwc4/procedural_dungeon_generation_algorithm_explained/ and check out the description and implementation. There's no source code available, so here's my approach to this problem in AS3 (works exactly the same, but keeps rectangles snapped to grid's resolution):
public class RoomSeparator extends AbstractAction {
public function RoomSeparator(name:String = "Room Separator") {
super(name);
}
override public function get finished():Boolean { return _step == 1; }
override public function step():void {
const repelDecayCoefficient:Number = 1.0;
_step = 1;
var count:int = _activeRoomContainer.children.length;
for(var i:int = 0; i < count; i++) {
var room:Room = _activeRoomContainer.children[i];
var center:Vector3D = new Vector3D(room.x + room.width / 2, room.y + room.height / 2);
var velocity:Vector3D = new Vector3D();
for(var j:int = 0; j < count; j++) {
if(i == j)
continue;
var otherRoom:Room = _activeRoomContainer.children[j];
var intersection:Rectangle = GeomUtil.rectangleIntersection(room.createRectangle(), otherRoom.createRectangle());
if(intersection == null || intersection.width == 0 || intersection.height == 0)
continue;
var otherCenter:Vector3D = new Vector3D(otherRoom.x + otherRoom.width / 2, otherRoom.y + otherRoom.height / 2);
var diff:Vector3D = center.subtract(otherCenter);
if(diff.length > 0) {
var scale:Number = repelDecayCoefficient / diff.lengthSquared;
diff.normalize();
diff.scaleBy(scale);
velocity = velocity.add(diff);
}
}
if(velocity.length > 0) {
_step = 0;
velocity.normalize();
room.x += Math.abs(velocity.x) < 0.5 ? 0 : velocity.x > 0 ? _resolution : -_resolution;
room.y += Math.abs(velocity.y) < 0.5 ? 0 : velocity.y > 0 ? _resolution : -_resolution;
}
}
}
}
I really like b005t3r's implementation! It works in my test cases, however my rep is too low to leave a comment with the 2 suggested fixes.
You should not be translating rooms by single resolution increments, you should translate by the velocity you just pain stakingly calculated! This makes the separation more organic as deeply intersected rooms separate more each iteration than not-so-deeply intersecting rooms.
You should not assume velociites less than 0.5 means rooms are separate as you can get stuck in a case where you are never separated. Imagine 2 rooms intersect, but are unable to correct themselves because whenever either one attempts to correct the penetration they calculate the required velocity as < 0.5 so they iterate endlessly.
Here is a Java solution (: Cheers!
do {
_separated = true;
for (Room room : getRooms()) {
// reset for iteration
Vector2 velocity = new Vector2();
Vector2 center = room.createCenter();
for (Room other_room : getRooms()) {
if (room == other_room)
continue;
if (!room.createRectangle().overlaps(other_room.createRectangle()))
continue;
Vector2 other_center = other_room.createCenter();
Vector2 diff = new Vector2(center.x - other_center.x, center.y - other_center.y);
float diff_len2 = diff.len2();
if (diff_len2 > 0f) {
final float repelDecayCoefficient = 1.0f;
float scale = repelDecayCoefficient / diff_len2;
diff.nor();
diff.scl(scale);
velocity.add(diff);
}
}
if (velocity.len2() > 0f) {
_separated = false;
velocity.nor().scl(delta * 20f);
room.getPosition().add(velocity);
}
}
} while (!_separated);
Here's an algorithm written using Java for handling a cluster of unrotated Rectangles. It allows you to specify the desired aspect ratio of the layout and positions the cluster using a parameterised Rectangle as an anchor point, which all translations made are oriented about. You can also specify an arbitrary amount of padding which you'd like to spread the Rectangles by.
public final class BoxxyDistribution {
/* Static Definitions. */
private static final int INDEX_BOUNDS_MINIMUM_X = 0;
private static final int INDEX_BOUNDS_MINIMUM_Y = 1;
private static final int INDEX_BOUNDS_MAXIMUM_X = 2;
private static final int INDEX_BOUNDS_MAXIMUM_Y = 3;
private static final double onCalculateMagnitude(final double pDeltaX, final double pDeltaY) {
return Math.sqrt((pDeltaX * pDeltaX) + (pDeltaY + pDeltaY));
}
/* Updates the members of EnclosingBounds to ensure the dimensions of T can be completely encapsulated. */
private static final void onEncapsulateBounds(final double[] pEnclosingBounds, final double pMinimumX, final double pMinimumY, final double pMaximumX, final double pMaximumY) {
pEnclosingBounds[0] = Math.min(pEnclosingBounds[BoxxyDistribution.INDEX_BOUNDS_MINIMUM_X], pMinimumX);
pEnclosingBounds[1] = Math.min(pEnclosingBounds[BoxxyDistribution.INDEX_BOUNDS_MINIMUM_Y], pMinimumY);
pEnclosingBounds[2] = Math.max(pEnclosingBounds[BoxxyDistribution.INDEX_BOUNDS_MAXIMUM_X], pMaximumX);
pEnclosingBounds[3] = Math.max(pEnclosingBounds[BoxxyDistribution.INDEX_BOUNDS_MAXIMUM_Y], pMaximumY);
}
private static final void onEncapsulateBounds(final double[] pEnclosingBounds, final double[] pBounds) {
BoxxyDistribution.onEncapsulateBounds(pEnclosingBounds, pBounds[BoxxyDistribution.INDEX_BOUNDS_MINIMUM_X], pBounds[BoxxyDistribution.INDEX_BOUNDS_MINIMUM_Y], pBounds[BoxxyDistribution.INDEX_BOUNDS_MAXIMUM_X], pBounds[BoxxyDistribution.INDEX_BOUNDS_MAXIMUM_Y]);
}
private static final double onCalculateMidpoint(final double pMaximum, final double pMinimum) {
return ((pMaximum - pMinimum) * 0.5) + pMinimum;
}
/* Re-arranges a List of Rectangles into something aesthetically pleasing. */
public static final void onBoxxyDistribution(final List<Rectangle> pRectangles, final Rectangle pAnchor, final double pPadding, final double pAspectRatio, final float pRowFillPercentage) {
/* Create a safe clone of the Rectangles that we can modify as we please. */
final List<Rectangle> lRectangles = new ArrayList<Rectangle>(pRectangles);
/* Allocate a List to track the bounds of each Row. */
final List<double[]> lRowBounds = new ArrayList<double[]>(); // (MinX, MinY, MaxX, MaxY)
/* Ensure Rectangles does not contain the Anchor. */
lRectangles.remove(pAnchor);
/* Order the Rectangles via their proximity to the Anchor. */
Collections.sort(pRectangles, new Comparator<Rectangle>(){ #Override public final int compare(final Rectangle pT0, final Rectangle pT1) {
/* Calculate the Distance for pT0. */
final double lDistance0 = BoxxyDistribution.onCalculateMagnitude(pAnchor.getCenterX() - pT0.getCenterX(), pAnchor.getCenterY() - pT0.getCenterY());
final double lDistance1 = BoxxyDistribution.onCalculateMagnitude(pAnchor.getCenterX() - pT1.getCenterX(), pAnchor.getCenterY() - pT1.getCenterY());
/* Compare the magnitude in distance between the anchor and the Rectangles. */
return Double.compare(lDistance0, lDistance1);
} });
/* Initialize the RowBounds using the Anchor. */ /** TODO: Probably better to call getBounds() here. **/
lRowBounds.add(new double[]{ pAnchor.getX(), pAnchor.getY(), pAnchor.getX() + pAnchor.getWidth(), pAnchor.getY() + pAnchor.getHeight() });
/* Allocate a variable for tracking the TotalBounds of all rows. */
final double[] lTotalBounds = new double[]{ Double.POSITIVE_INFINITY, Double.POSITIVE_INFINITY, Double.NEGATIVE_INFINITY, Double.NEGATIVE_INFINITY };
/* Now we iterate the Rectangles to place them optimally about the Anchor. */
for(int i = 0; i < lRectangles.size(); i++) {
/* Fetch the Rectangle. */
final Rectangle lRectangle = lRectangles.get(i);
/* Iterate through each Row. */
for(final double[] lBounds : lRowBounds) {
/* Update the TotalBounds. */
BoxxyDistribution.onEncapsulateBounds(lTotalBounds, lBounds);
}
/* Allocate a variable to state whether the Rectangle has been allocated a suitable RowBounds. */
boolean lIsBounded = false;
/* Calculate the AspectRatio. */
final double lAspectRatio = (lTotalBounds[BoxxyDistribution.INDEX_BOUNDS_MAXIMUM_X] - lTotalBounds[BoxxyDistribution.INDEX_BOUNDS_MINIMUM_X]) / (lTotalBounds[BoxxyDistribution.INDEX_BOUNDS_MAXIMUM_Y] - lTotalBounds[BoxxyDistribution.INDEX_BOUNDS_MINIMUM_Y]);
/* We will now iterate through each of the available Rows to determine if a Rectangle can be stored. */
for(int j = 0; j < lRowBounds.size() && !lIsBounded; j++) {
/* Fetch the Bounds. */
final double[] lBounds = lRowBounds.get(j);
/* Calculate the width and height of the Bounds. */
final double lWidth = lBounds[BoxxyDistribution.INDEX_BOUNDS_MAXIMUM_X] - lBounds[BoxxyDistribution.INDEX_BOUNDS_MINIMUM_X];
final double lHeight = lBounds[BoxxyDistribution.INDEX_BOUNDS_MAXIMUM_Y] - lBounds[BoxxyDistribution.INDEX_BOUNDS_MINIMUM_Y];
/* Determine whether the Rectangle is suitable to fit in the RowBounds. */
if(lRectangle.getHeight() <= lHeight && !(lAspectRatio > pAspectRatio && lWidth > pRowFillPercentage * (lTotalBounds[BoxxyDistribution.INDEX_BOUNDS_MAXIMUM_X] - lTotalBounds[BoxxyDistribution.INDEX_BOUNDS_MINIMUM_X]))) {
/* Register that the Rectangle IsBounded. */
lIsBounded = true;
/* Update the Rectangle's X and Y Co-ordinates. */
lRectangle.setFrame((lRectangle.getX() > BoxxyDistribution.onCalculateMidpoint(lBounds[BoxxyDistribution.INDEX_BOUNDS_MAXIMUM_X], lBounds[BoxxyDistribution.INDEX_BOUNDS_MINIMUM_X])) ? lBounds[BoxxyDistribution.INDEX_BOUNDS_MAXIMUM_X] + pPadding : lBounds[BoxxyDistribution.INDEX_BOUNDS_MINIMUM_X] - (pPadding + lRectangle.getWidth()), lBounds[1], lRectangle.getWidth(), lRectangle.getHeight());
/* Update the Bounds. (Do not modify the vertical metrics.) */
BoxxyDistribution.onEncapsulateBounds(lTotalBounds, lRectangle.getX(), lBounds[BoxxyDistribution.INDEX_BOUNDS_MINIMUM_Y], lRectangle.getX() + lRectangle.getWidth(), lBounds[BoxxyDistribution.INDEX_BOUNDS_MINIMUM_Y] + lHeight);
}
}
/* Determine if the Rectangle has not been allocated a Row. */
if(!lIsBounded) {
/* Calculate the MidPoint of the TotalBounds. */
final double lCentreY = BoxxyDistribution.onCalculateMidpoint(lTotalBounds[BoxxyDistribution.INDEX_BOUNDS_MAXIMUM_Y], lTotalBounds[BoxxyDistribution.INDEX_BOUNDS_MINIMUM_Y]);
/* Determine whether to place the bounds above or below? */
final double lYPosition = lRectangle.getY() < lCentreY ? lTotalBounds[BoxxyDistribution.INDEX_BOUNDS_MINIMUM_Y] - (pPadding + lRectangle.getHeight()) : (lTotalBounds[BoxxyDistribution.INDEX_BOUNDS_MAXIMUM_Y] + pPadding);
/* Create a new RowBounds. */
final double[] lBounds = new double[]{ pAnchor.getX(), lYPosition, pAnchor.getX() + lRectangle.getWidth(), lYPosition + lRectangle.getHeight() };
/* Allocate a new row, roughly positioned about the anchor. */
lRowBounds.add(lBounds);
/* Position the Rectangle. */
lRectangle.setFrame(lBounds[BoxxyDistribution.INDEX_BOUNDS_MINIMUM_X], lBounds[BoxxyDistribution.INDEX_BOUNDS_MINIMUM_Y], lRectangle.getWidth(), lRectangle.getHeight());
}
}
}
}
Here's an example using an AspectRatio of 1.2, a FillPercentage of 0.8 and a Padding of 10.0.
This is a deterministic approach which allows spacing to occur around the anchor whilst leaving the location of the anchor itself unchanged. This allows the layout to occur around wherever the user's Point of Interest is. The logic for selecting a position is pretty simplistic, but I think the surrounding architecture of sorting the elements based upon their initial position and then iterating them is a useful approach for implementing a relatively predictable distribution. Plus we're not relying on iterative intersection tests or anything like that, just building up some bounding boxes to give us a broad indication of where to align things; after this, applying padding just comes kind of naturally.
Here is a version that takes cape1232's answer and is a standalone runnable example for Java:
public class Rectangles extends JPanel {
List<Rectangle2D> rectangles = new ArrayList<Rectangle2D>();
{
// x,y,w,h
rectangles.add(new Rectangle2D.Float(300, 50, 50, 50));
rectangles.add(new Rectangle2D.Float(300, 50, 20, 50));
rectangles.add(new Rectangle2D.Float(100, 100, 100, 50));
rectangles.add(new Rectangle2D.Float(120, 200, 50, 50));
rectangles.add(new Rectangle2D.Float(150, 130, 100, 100));
rectangles.add(new Rectangle2D.Float(0, 100, 100, 50));
for (int i = 0; i < 10; i++) {
for (int j = 0; j < 10; j++) {
rectangles.add(new Rectangle2D.Float(i * 40, j * 40, 20, 20));
}
}
}
List<Rectangle2D> rectanglesToDraw;
protected void reset() {
rectanglesToDraw = rectangles;
this.repaint();
}
private List<Rectangle2D> findIntersections(Rectangle2D rect, List<Rectangle2D> rectList) {
ArrayList<Rectangle2D> intersections = new ArrayList<Rectangle2D>();
for (Rectangle2D intersectingRect : rectList) {
if (!rect.equals(intersectingRect) && intersectingRect.intersects(rect)) {
intersections.add(intersectingRect);
}
}
return intersections;
}
protected void fix() {
rectanglesToDraw = new ArrayList<Rectangle2D>();
for (Rectangle2D rect : rectangles) {
Rectangle2D copyRect = new Rectangle2D.Double();
copyRect.setRect(rect);
rectanglesToDraw.add(copyRect);
}
// Find the center C of the bounding box of your rectangles.
Rectangle2D surroundRect = surroundingRect(rectanglesToDraw);
Point center = new Point((int) surroundRect.getCenterX(), (int) surroundRect.getCenterY());
int movementFactor = 5;
boolean hasIntersections = true;
while (hasIntersections) {
hasIntersections = false;
for (Rectangle2D rect : rectanglesToDraw) {
// Find all the rectangles R' that overlap R.
List<Rectangle2D> intersectingRects = findIntersections(rect, rectanglesToDraw);
if (intersectingRects.size() > 0) {
// Define a movement vector v.
Point movementVector = new Point(0, 0);
Point centerR = new Point((int) rect.getCenterX(), (int) rect.getCenterY());
// For each rectangle R that overlaps another.
for (Rectangle2D rPrime : intersectingRects) {
Point centerRPrime = new Point((int) rPrime.getCenterX(), (int) rPrime.getCenterY());
int xTrans = (int) (centerR.getX() - centerRPrime.getX());
int yTrans = (int) (centerR.getY() - centerRPrime.getY());
// Add a vector to v proportional to the vector between the center of R and R'.
movementVector.translate(xTrans < 0 ? -movementFactor : movementFactor,
yTrans < 0 ? -movementFactor : movementFactor);
}
int xTrans = (int) (centerR.getX() - center.getX());
int yTrans = (int) (centerR.getY() - center.getY());
// Add a vector to v proportional to the vector between C and the center of R.
movementVector.translate(xTrans < 0 ? -movementFactor : movementFactor,
yTrans < 0 ? -movementFactor : movementFactor);
// Move R by v.
rect.setRect(rect.getX() + movementVector.getX(), rect.getY() + movementVector.getY(),
rect.getWidth(), rect.getHeight());
// Repeat until nothing overlaps.
hasIntersections = true;
}
}
}
this.repaint();
}
private Rectangle2D surroundingRect(List<Rectangle2D> rectangles) {
Point topLeft = null;
Point bottomRight = null;
for (Rectangle2D rect : rectangles) {
if (topLeft == null) {
topLeft = new Point((int) rect.getMinX(), (int) rect.getMinY());
} else {
if (rect.getMinX() < topLeft.getX()) {
topLeft.setLocation((int) rect.getMinX(), topLeft.getY());
}
if (rect.getMinY() < topLeft.getY()) {
topLeft.setLocation(topLeft.getX(), (int) rect.getMinY());
}
}
if (bottomRight == null) {
bottomRight = new Point((int) rect.getMaxX(), (int) rect.getMaxY());
} else {
if (rect.getMaxX() > bottomRight.getX()) {
bottomRight.setLocation((int) rect.getMaxX(), bottomRight.getY());
}
if (rect.getMaxY() > bottomRight.getY()) {
bottomRight.setLocation(bottomRight.getX(), (int) rect.getMaxY());
}
}
}
return new Rectangle2D.Double(topLeft.getX(), topLeft.getY(), bottomRight.getX() - topLeft.getX(),
bottomRight.getY() - topLeft.getY());
}
public void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2d = (Graphics2D) g;
for (Rectangle2D entry : rectanglesToDraw) {
g2d.setStroke(new BasicStroke(1));
// g2d.fillRect((int) entry.getX(), (int) entry.getY(), (int) entry.getWidth(),
// (int) entry.getHeight());
g2d.draw(entry);
}
}
protected static void createAndShowGUI() {
Rectangles rects = new Rectangles();
rects.reset();
JFrame frame = new JFrame("Rectangles");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setLayout(new BorderLayout());
frame.add(rects, BorderLayout.CENTER);
JPanel buttonsPanel = new JPanel();
JButton fix = new JButton("Fix");
fix.addActionListener(new ActionListener() {
#Override
public void actionPerformed(ActionEvent e) {
rects.fix();
}
});
JButton resetButton = new JButton("Reset");
resetButton.addActionListener(new ActionListener() {
#Override
public void actionPerformed(ActionEvent e) {
rects.reset();
}
});
buttonsPanel.add(fix);
buttonsPanel.add(resetButton);
frame.add(buttonsPanel, BorderLayout.SOUTH);
frame.setSize(400, 400);
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
public static void main(String[] args) {
SwingUtilities.invokeLater(new Runnable() {
#Override
public void run() {
createAndShowGUI();
}
});
}
}