Author Topic: Arena (7DRL 2014) [FAILURE]  (Read 9639 times)

SoulBeaver

  • Newcomer
  • Posts: 20
  • Karma: +0/-0
    • View Profile
    • Email
Re: Arena (7DRL 2014)
« Reply #15 on: March 11, 2014, 01:03:49 PM »
Not too much to report this time. I gave the game a different skin (couresy of Reiner's skins: http://www.reinerstilesets.de/) and it looks a lot better now, albeit the walls are still piss-ugly.



I ran into a performance bottleneck recently. My grid is 100x100 large, or 10,000 tiles. I didn't mind it at the time, but rendering the entire map at every call is incredibly expensive! My framerate dropped below thirty and with the additional pressure of logging it stooped to a meager 15-17fps. Absolutely horrible- something has to be done!

I've since calculated the effective viewing area by working out a rectangle with my player in the center. Anything outside of this rectangle is not drawn. It works fine enough, even for edge cases such as the player being in a corner, but the code is horrendously long and ugly. I'll have to refactor it, but I'm unsure of what better methods exist for such calculations. Maybe somebody could provide assistance?

Code: [Select]
private fun calculateViewArea(): Rectangle {
        val playerCoordinates = level.playerCoordinates

        val leftTilesAvailable   = playerCoordinates.x
        val rightTilesAvailable  = level.width - playerCoordinates.x

        val topTilesAvailable    = playerCoordinates.y
        val bottomTilesAvailable = level.height - playerCoordinates.y

        if (leftTilesAvailable >= 15 && rightTilesAvailable >= 15 &&
            topTilesAvailable >= 15 && bottomTilesAvailable >= 15) {
            return Rectangle(playerCoordinates.let { Point(it.x - 15, it.y - 15) },
                             playerCoordinates.let { Point(it.x + 15, it.y + 15) })
        }

        var numberOfLeftTiles  = if (leftTilesAvailable >= 15)  15 else leftTilesAvailable
        var numberOfRightTiles = if (rightTilesAvailable >= 15) 15 else rightTilesAvailable

        if (numberOfLeftTiles < 15)
            numberOfRightTiles += 15 - numberOfLeftTiles

        if (numberOfRightTiles < 15)
            numberOfLeftTiles += 15 - numberOfRightTiles

        var numberOfTopTiles    = if (topTilesAvailable >= 15)    15 else topTilesAvailable
        var numberOfBottomTiles = if (bottomTilesAvailable >= 15) 15 else bottomTilesAvailable

        if (numberOfTopTiles < 15)
            numberOfBottomTiles += 15 - numberOfTopTiles

        if (numberOfBottomTiles < 15)
            numberOfTopTiles += 15 - numberOfBottomTiles

        val start = Point(playerCoordinates.x - numberOfLeftTiles,
                playerCoordinates.y - numberOfTopTiles)
        val end = Point(playerCoordinates.x + numberOfRightTiles,
                playerCoordinates.y + numberOfBottomTiles)

        return Rectangle(start, end)
    }

Otherwise I smoothed out the input handling. Every input now comes with its type, whether it was pressed or held down. This is useful, because I want a player to keep moving if they keep the key pressed, but not to keep eating walls. That's now possible, yay!

SoulBeaver

  • Newcomer
  • Posts: 20
  • Karma: +0/-0
    • View Profile
    • Email
Re: Arena (7DRL 2014)
« Reply #16 on: March 11, 2014, 07:51:00 PM »
Oof, today was a lackluster day. I didn't accomplish everything I wanted to do. There were a few bottlenecks that prevented extending my code with the desired features. The first was a performance issue, which I solved in my previous post. The next one was a little harder to reason about.

Currently, the block moves at a speed of 20px per frame. That's breakneck speed! It's much too difficult to control the player, so I decided I would implement some animation between each movement. Even though it's "just a blue block", the Player will one day feature animations and I should factor that in. However, in what order should events occur?

Assume the player is at (1, 1) and wishes to move right to (2, 1). That is one block, technically an atomic move with no wiggle room in between. Yet I need to transition the player from one block to the other as if it were a fluid movement. I figured that it's best to display the animation first, and when it's complete, i.e. the player is visually on the next grid, I change the player's position to reflect the change on the screen.

So far so good, but my code was having none of it. A function accepting (level) was absolutely ill-equipped to deal with adding animations. Where and how? I needed a change.

Therefore, I've changed my architecture from a single-state game to a state machine. I've the Game State and during each move I change the state to Playing, or something. It's still conceptual, but I see no reason why that shouldn't work. It's the exact same as the Game State, but input is disallowed and an animation is playing. Maybe Slicks can stack states...

Anyhow! This let me implement a menu at least:



The background is not made by me. In fact, I just... kinda took it. The wallpaper can be found here: http://www.wallpick.com/paint-black-wallpaper.html If it turns out that I probably shouldn't do this (I don't think I should), then I'll replace it with something free like Reiner's tiles.

A Victory and Defeat screen is also planned and should be quickly completed, but aren't yet.
« Last Edit: March 11, 2014, 07:52:42 PM by SoulBeaver »

SoulBeaver

  • Newcomer
  • Posts: 20
  • Karma: +0/-0
    • View Profile
    • Email
Re: Arena (7DRL 2014)
« Reply #17 on: March 18, 2014, 08:25:57 AM »
*sigh* I'm sorry, this project was a failure. I went ahead and rewrote the game into states to allow for correct input handling and animation that was homogenous for both players and enemies. On Thursday I received work for my research project which kept me busy well into Sunday, resulting in little to no further updates on the game. It was going well, I suppose, but this project is now officially a failure.

I completed development yesterday, using an illegal eighth day, to boot the game back into a playable state. Here's the changes:

1. States

There is now a turn-based system baked into the game. We transition from the Main Menu into the PlayerTurnState. We remain there until the user enters a command at which point we execute that command and transition into the EnemyState.

2. Animation

Every Input is called an InputRequest. It contains all information necessary to create an animation out of it, so my toAnimation(InputRequest) method does just that. From there, an AnimationPlayer queues and plays all pending animations remaining for a given state. For example, if the player moves left, then a MoveRequest is created, turned into a MoveAnimation and then played. After it's done, the EnemyTurnState is invoked.

I like this solution because it uses what I believe is called Continuation Passing Style. It's a functional idiom whose counterpart can be best explained with the Command Pattern. The Command Pattern is just a wrapper because free-floating functions usually aren't allowed and thus everything must be wrapped in a class.

This is all a little theoretical, here is some code:

We have our InputRequests:

Code: [Select]
trait InputRequest {
    fun initialize()
    fun execute()
    fun isValid(): Boolean
}

fun initialize(initializer: () -> InputRequest): () -> InputRequest {
    return {
        val request = initializer()
        request.initialize()
        request
    }
}

Which is used to initialize my keymap:

Code: [Select]
    inputRequests = mapOf(
                configuration.moveUp    to initialize { MoveRequest(level, player, Direction.North) },
                configuration.moveDown  to initialize { MoveRequest(level, player, Direction.South) },
                configuration.moveLeft  to initialize { MoveRequest(level, player, Direction.West) },
                configuration.moveRight to initialize { MoveRequest(level, player, Direction.East) },

                configuration.toggleWallUp    to initialize { ToggleWallRequest(level, player, Direction.North) },
                configuration.toggleWallDown  to initialize { ToggleWallRequest(level, player, Direction.South) },
                configuration.toggleWallLeft  to initialize { ToggleWallRequest(level, player, Direction.West) },
                configuration.toggleWallRight to initialize { ToggleWallRequest(level, player, Direction.East) },

                configuration.shoot to initialize { ShootRequest(level, player, enemies) }
        )

and then acted on whenever an input is received:

Code: [Select]
    override fun update(gameContainer: GameContainer?,
                        game: StateBasedGame?,
                        delta: Int) {
        if (!renderer.hasAnimationsPlaying()) {
            renderer.onAllAnimationsFinished { game!!.enterState(EnemyTurnState.ID) }

            enteredInputs(gameContainer!!).filter  { it.isValid() }
                                          .forEach { renderer.play(toAnimation(it)) }
        }

        renderer.update()
    }

    private fun enteredInputs(gameContainer: GameContainer): List<InputRequest> {
        val isKeyPressed = { (key: String) -> inputController.isKeyPressed(gameContainer, key) }

        return inputRequests.entrySet()
                            .filter { isKeyPressed(it.getKey()) }
                            .map { it.getValue()() }
    }

In this last snippet you can see how the code is glued together. It's pretty terse, so I'll take it step-by-step.

Code: [Select]
    if (!renderer.hasAnimationsPlaying()) {
            // ...
        }

A State is split into two sub-states:  accepting input and animating. If we're not animating, we're accepting input and vice-versa. When we enter a state, then we first wait for input, and need to make sure that all further calls are not accepting input.

Code: [Select]
renderer.onAllAnimationsFinished { game!!.enterState(EnemyTurnState.ID) }
onAllAnimationsFinished is a function which accepts a function- Higher Order Functions is the concept. I'm telling the renderer what to do when all animations have finished. This inversion of control, my PlayerTurnState telling the Renderer to do, not letting the Renderer decide what happens, is called Continuation Passing Style. A difficult name for a simple concept :)

Code: [Select]
enteredInputs(gameContainer!!).filter  { it.isValid() }
                                          .forEach { renderer.play(toAnimation(it)) }

I poll the keyboard for all relevant inputs (the keys in the map) and make sure that all entered commands are valid. For all valid commands I queue an animation to be played.

That's it :) This lets me handle a multitude of commands- right now we have shooting, moving and toggling walls and floors. I wasn't able to implement enemies yet, even though they ARE in the code. Well, a very primitive version of them.

3. Shooting

Myes, that's possible now. Not much to say, it's very basic and there are no enemies yet.

4. Conclusion

I learned a lot. As I said in the beginning, this was my first roguelike and, to be honest, my first real attempt at programming a game. No other game came as far as this one- even though this one didn't even come that far! However, for as ashamed as I am in the bitter defeat I've suffered here, it was great fun. I'll participate again and with all of this new knowledge and these new tools, I'll be able to create a better game in less time. Fate permitting, I won't be pulled off the contest again, but who knows :)

As usual, all code can be found on Github:  https://github.com/SoulBeaver/7DRL

That's my time, take care guys! Congratulations to all of you that finished, what great entries! :)

koiwai

  • Rogueliker
  • ***
  • Posts: 99
  • Karma: +0/-0
    • View Profile
Re: Arena (7DRL 2014) [FAILURE]
« Reply #18 on: March 18, 2014, 03:56:34 PM »
Regarding the view area. If I understand your idea, you want to avoid displaying anything what is outside the map, so when the player is in the middle of the map, he is shown in the center of the screen, but when he approaches the corner, the camera stops moving and the player ends up in the corner of the screen, as show in the screenshot.

If this is correct, it seems to me that the following code should work:
Code: [Select]
Let (px, py) by the position if the player.
Let (w,h) be the width and the height of the map.
Let d = 15.

// the center of the visible area, the "camera" so to say
cx = min(max(px,d), w-d-1)
cy = min(max(py,d), h-d-1) 

// the corner of the visible area
x0 = cx - d
y0 = cy - d

Then, to output, you iterate the points:
x = x0, ... (x0+2d+1), and
y = y0, ... (y0+2d+1)

I'm curious, why did you choose Kotlin as the implementation language?

SoulBeaver

  • Newcomer
  • Posts: 20
  • Karma: +0/-0
    • View Profile
    • Email
Re: Arena (7DRL 2014) [FAILURE]
« Reply #19 on: March 18, 2014, 05:49:43 PM »
Hi koiwai,

thanks for your input! I meant to refactor the gargantuan view area method, but since it worked and I didn't really want to change something that worked. Your implementation looks a lot prettier though, and a post-7drl refactoring should be fine, so I'll give it a try :)

As for why I chose Kotlin, there are a few reasons. Java is much too verbose and, in my opinion, not suited for game development. I might have used Java 8, but it's not really here yet and I didn't feel like imposing Java 8 as that might break my platform independence (can Linux do Java 8?)

I could've gone with C++11, as that is another of my stronger languages, but, for as awesome I consider the language to be, it's not as easy to just quickly bash out code as it is in a language such as Java or Kotlin. Granted, C++ is magnitudes more performant, but I felt that a 2D roguelike doesn't need that sort of engine.

While I'm not as experienced in it, Python or Ruby could have suited this project as well. I'm better writing Python than Ruby, so I'll talk about that. The discomfort of not having mastered the language was most definitely a factor. It was a seven day roguelike and I didn't want to gamble. I'm also a fan of static typing. Nevertheless, Python inspired a few snippets in my code, most notably the Configuration intialization by map which I think is a terrific feature in Python. It might've been nice to had a few other functionalities from Python available- maybe next time.

Finally, I could've programmed in F#, my one functional dialect. Quite frankly, this is the language I'm least competent in. I love the immutability, the function composition and piping the output of one function to the next. You can see quite a few places in Kotlin where I do exactly that. As soon as I'm competent enough I'll give a 7DRL with F# a try and see how that pans out :)

Kotlin is a young language, but the functional features it provides hits a sweet spot for me. The (almost) seamless interop with all java libraries means I still get the full technology stack, which is great. Plus, it boasts free-floating functions which is a must. I immensely dislike the trend of shoehorning everything into a class when it doesn't need to be. That is one of its greatest assets in my opinion. Plus, it's statically typed, and I like that. No gripes with Python or the other dynamic languages, just a personal preference.

I'm not entirely satisfied with Kotlin though. The C-style syntax is pretty ugly in my opinion and there are large issues to address- I can't get the bloody thing to work outside of the IntelliJ IDE which is horrible if you're trying to show the game to other people. I'm still looking for a language that will really get me to say "Oooooooooooh look at that sexy language." Elixir and Fancy are exceptionally close, but neither are suited for game development.

tl;dr Kotlin is statically typed, has functional idioms and almost seamless Java interop. It's not perfect, still looking for a better language (e.g. Elixir or Fancy), but that's primarily why I chose it :)

SoulBeaver

  • Newcomer
  • Posts: 20
  • Karma: +0/-0
    • View Profile
    • Email
Re: Arena (7DRL 2014) [FAILURE]
« Reply #20 on: March 18, 2014, 06:16:35 PM »
Your calculation was spot on koiwai! It's implemented and working beautifully :)

koiwai

  • Rogueliker
  • ***
  • Posts: 99
  • Karma: +0/-0
    • View Profile
Re: Arena (7DRL 2014) [FAILURE]
« Reply #21 on: March 18, 2014, 11:14:32 PM »
Your calculation was spot on koiwai! It's implemented and working beautifully :)
Glad to hear that!
Yeah, there is no perfect language ) Just pick the one you enjoy the most. I usually program in OCaml or in C.

Also, I think, you did pretty well, considering that this is your first serious attempt to make a game, and there was some unexpected work that had to be done.