views:

349

answers:

3

Currently writing a roguelike to learn more about Objective-C/Cocoa. I'm really enjoying it so far and I've learned tons.

This code moves the origin of the view's bounds, so that it follows the player as he moves. The code works perfect, I was just asking if there was a better way than using four for's.

I've also seen in some cases that it's better to do things separately instead of all in one, especially where drawing and nsbezierpath is concerned, why is that?

Edit: People were having trouble figuring out exactly what I'm doing, so I'm going to break it down as much as I can.

The view is a 30x30 grid (0-29,0-29) of tiles, 20x20 each. The map can be as big or small as needs to be.

First, you get the [player_ location], along with the origin of the bounds for the view. It's divided by 20 because the tile size is 20, so when it's at (1,2), it's actually at (20,40). The only reason I do this is to make it easier to manipulate (It's easier to count in units of 1 as opposed to 20). The four for's go through and check that the [player_ location] is within 15 tiles of the center (bounds + 15) of the view. If the player is moving towards one of the edges of the screen, and bounds.x/y + 30 is less than the height of the current map/width, it moves the origin so that the player is still centered and displayed on the map.

The code works perfect, and I moved the setbounds to after the for's happen, and there's only one. It isn't being left in drawRect, I just had it here to try and figure out what I needed to do. It's now in it's own place and is only called when the player actually moves.

Here's the new code:

- (void)keepViewCentered
{
 NSPoint pl = [player_ location];
 NSPoint ll;
 NSPoint location = [self bounds].origin;
 ll.x = location.x / 20;
 ll.y = location.y / 20;
 for ( pl.y = [player_ location].y; pl.y >= ll.y + 15 && ll.y + 30 < [currentMap_ height]; ll.y++ )
 {
  location.y = ll.y * 20;
 }
 for ( pl.x = [player_ location].x; pl.x >= ll.x + 15 && ll.x + 30 < [currentMap_ width]; ll.x++ )
 {
  location.x = ll.x * 20;
 }
 for ( pl.y = [player_ location].y; pl.y <= ll.y + 15 && ll.y >= 0; ll.y-- )
 {
  location.y = ll.y * 20;
 }
 for ( pl.x = [player_ location].x; pl.x <= ll.x + 15 && ll.x >= 0; ll.x-- )
 {
  location.x = ll.x * 20;
 }
 [self setBoundsOrigin: location];
}

Here are pictures of it in action!

Figure 1: This is the player at 1,1. Nothing special.

Figure 2: The 3 gold pieces represent how far the player can move before the view's bounds.origin will move to stay centered on the player. Although irrelevant, notice that the player cannot actually see the gold. Turns out programming field of vision is a field of much debate and there are several different algorithms you can use, none of which don't have downsides, or have been ported to Objective-C. Currently it's just a square. See through walls and everything.

Figure 3: The view with a different bounds.origin, centered on the player.

+1  A: 

I'm not clear what you're trying to do here, but it's very, very inefficient. You're calling setBoundsOrigin: repeatedly, which means that only the last call actually is doing anything. Setting your origin in the middle of drawRect: doesn't cause anything to move. An entire run of drawRect: draws a single "frame" if you're thinking of this as an animation.

Remember, you're not in charge of the drawing loop. drawRect: gets called by the framework when it decides that drawing is required. There are various hints you can give to the framework (like setNeedsDisplay:), but ultimately it's the framework who calls you when it needs something.

If your intent is animation, you'll want to read the Core Animation Programming Guide. It'll discuss very easy and efficient ways to manage animations. Animating things around the screen is one of Cocoa's great strengths, and quite complex things can be done very easily.

Rob Napier
It's pretty obvious. There's a 30x30 view, the player is navigating a 90x90 map. I'm changing the setboundsorigin so that the view is centered on the player. It's only going to call setBoundsOrigin if one of those 4 things happen, and no more than 2 of them can happen at once. The code works perfect, I was just wondering what could be done in the way of refactoring.
Sneakyness
In terms of refactoring, this should be done outside of drawRect: which gets called many times. These things should be calculated one time when [player_ location] changes. Hiding your conditionals in for() loops that will never execute does not make for readable code, and while it possible to decipher, I still believe it is not clear (from the code) what you're trying to do.
Rob Napier
Yeah, Rob is right. Your code is not clear, and your solution sucks. It took me a minute or two to figure out what you are doing, and now that I know, I am horrified. `drawRect:` should draw *the entire view*, not one tile, and it most assuredly should not set `self` as needing display again! I think you should look into a layer-based solution, with your view hosting a root layer containing layers for background tiles and items, and letting all of those layers do the drawing.
Peter Hosey
It is drawing the entire view, what are you talking about? Do I need to draw a diagram? It's really not that hard to grasp at all.
Sneakyness
A: 

in some cases … it's better to do it separately instead of all in one …

Yes. Have a drawBackgroundTile: method, and maybe a drawItem: method and a drawPlayer: method, and call these in a loop within drawRect:—which should draw the entire board for the level. That's the non-layer-based solution.

The other one, as I noted in my comment on Rob's answer, is to use Core Animation. You would then axe drawRect: altogether, and have your view host a root layer, which would contain tile layers, some of which would contain item and actor (player/monster/pet) layers.

Why is that?

Because it's a proper, efficient, and readable solution.

Peter Hosey
Well considering this is the first thing I've ever written in Objective-C, I'm more concerned about getting the basics down before I go about doing what you're talking about. Thankfully I do have the world/characters/items all separate, so it won't be hard to do this once I decide to.
Sneakyness
Hey Pete, I rewrote the question, and added example pictures showing what it does. Let me know if you need any more clarification.
Sneakyness
+1  A: 

It's now in it's own place and is only called when the player actually moves.

Yay!

I was just asking if there was a better way than using four for's.

Here's my suggestion:

  1. Set the lower-left to the player's location minus 15.
    ll.x = [player_ location].x - 15.0;
    ll.y = [player_ location].y - 15.0;
  2. Make sure that it does not exceed the bounds of the view.
    if (ll.x < 0.0)
        ll.x = 0.0;
    if (ll.y < 0.0)
        ll.y = 0.0;
    if (ll.x > [currentMap_ width] - 30.0)
        ll.x = [currentMap_ width] - 30.0;
    if (ll.y > [currentMap_ height] - 30.0)
        ll.y = [currentMap_ height] - 30.0;
  3. Multiply by your tile size and set as the new bounds origin. location.x = ll.x * 20.0; location.y = ll.y * 20.0; [self setBoundsOrigin: location];

I also suggest that you not hard-code the tile size and player sight range.

For the tile size, you may prefer to give your view a couple of properties expressing width and height in tiles, then using the CTM to scale to (widthInPoints / widthInTiles), (heightInPoints / heightInTiles). Then you can let the user resize your window to change the tile size.

For the player's sight range (currently 30 tiles square), you may want to make this a property of the player, so that it can change with skill stats, items equipped, and the effects of potions, monster attacks, and wraths of the gods. One caveat: If the player's sight range can become longer than the game window will fit (especially vertically), you'll want to add a check for that to the above code, and handle it by putting the player dead-center and possibly zooming out.

Peter Hosey
I'm really not looking to make the window resizable. To be blunt I hate all this floppy windowed bullshit resize this and that. It's the size I want it to be and it's going to stay that way.The code is in place though, like I said I've just now gotten around to smoothing things out, and both the sight radius and the cell width/height are stored where they should be (player and map, respectively). The player's sight radius is set to three. If you can't see it, you only see a faded out remembered version of what you saw there last.
Sneakyness
I was wondering where you got the idea it was hardcoded and I see now. I take care to test everything as simply as I can, to make sure I'm not making any mistakes. I do however like your idea for handling it, and I actually know how to make that all one condition, which is lovely. As far as the sight-square goes, I hate it, and you should be expecting a few posts from me soon about some pretty serious algorithm questions. It's much more complex than you would expect it to be.
Sneakyness
On a third and final thought, once everything else is where I'd like it to be, I'll be setting up the view so that when it is able to be resized, it will display more tiles instead of changing the size. I'm going to be offering a tiled version like this, and a character version like any other roguelike.
Sneakyness
You win for now though, if anybody else comes along and knows of a better way to implement this, I'd love to hear about it.
Sneakyness