Home > other >  How would you programmatically search for an object an array by name of the object in Kotlin
How would you programmatically search for an object an array by name of the object in Kotlin

Time:06-16

I need to do some complicated things for an app I just started making, and I don't know how I would do a lot of it. Here's how everything works. I have an object containing "rooms" that the player has generated and explored. Each of these "rooms" is another object containing the data for that specific room, like its location, or items in that room. There is also one array in the first object, which contains all the "room" objects. These objects will be programmatically created when the user generates a new room they have not visited before, and the name of the object will be generated based on the location of the room(which I also don't know how to do, but that's a future issue).

The first thing that I need to do is search the array inside that first object called "Rooms" for another object whose name matches the player's location. This is determined by 3 variables, that determine where the player is on the grid of rooms. So 0, 0, 0 would be the starting location, and for example, if the player travels 1 room right, 2 rooms backwards, and 1 story up, the player's location would be X: 1, Y: -2, Z: 1. This room's object name would be "R1_n2_1" so the game will search the array for an object called "R1_n2_1" and load it if it is found. But if there is no object with the matching name, then it will generate a new room and add this new room's data as another object whose name matches what it was looking for("R1_n2_1"), and then the new object will also be added to the array specifying all these "rooms"/objects(as mentioned previously, I'll figure out that later).

What I need is how I would search for that objects name by programmatically generating the object name I am looking for and then finding an object whose name matches that. For example, my current code, which almost certainly wont work but communicates the idea, generates a string of the object name it is looking for, then searches the array for a match. However, it will never find this match because it is looking for a string, not an object with a matching name.

class GameActivity : AppCompatActivity() {

private var exitedFrom = "start" //start; l; r; t; b; tunnel; ladder; ?;
private var Xcord: Long = 0
private var Ycord: Long = 0
private var Zcord: Long = 0

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_game)

    fullscreen()
    generateRoom()
}

private fun generateRoom() {

    val room = "R$Xcord" "_$Ycord" "_$Zcord"
    if (Rooms.rooms.contains(room)) {
        //load the room
        println("room $room found")
    } else println("room $room not found.") //result is always this  

}

Here is my Object for all the rooms. Template is what will be cloned to generate a new room and R0_0_0 is the starting room.

object Rooms {

    object Template {
    var x = 0
    var y = 0
    var z = 0
    var exitT = false
    var exitB = false
    var exitL = false
    var exitR = false
    }
    
    object R0_0_0 { //R(x)_(y)_(z)
    var x = 0
    var y = 0
    var z = 0
    var exitT = true
    var exitB = true
    var exitL = true
    var exitR = true
    }

    var rooms = arrayListOf(Template, R0_0_0)
    
}

Update: My first idea I had was to literally not care about the object name at all and search every object listed in the array for matching location variables(x, y, z). However, this doesn't work since once the array has more than one object, it cant search everything in the array for match in any of its variables. Here is that function:

    private fun generateRoom() {

    Rooms.rooms.forEach {
        if(it.x == Xcord && it.y == Ycord && it.z == Zcord) {
            println("Room Found")
        }
    }
}

Is there a way around this or should I do something else? Would a "data class" work instead of objects? I dont know how data classes work, but they sound promising. I've already started trying something but I have no idea how it will work. Any other ideas or working solutions would be very helpful though!
One last question: Can you save these objects with SharedPreferences or online databases and if yes, how?

CodePudding user response:

A few things I want to point out first:

  • You should avoid wherever possible using objects that hold mutable state (var properties or val properties referencing mutable class instances). These tend to make code fragile (easy to introduce bugs) because shared state can so easily be changed from anywhere, and they make it very hard to write test code. They can also cause memory leaks if you aren't careful with them.
  • Even if you changed Template and R0_0_0 into classes, they are kind of a nonsensical design. They have the same properties, so they should be the same class. And it doesn't make sense to have the coordinates in the class name. If you have a 10x10x10 grid, are you going to manually define 1000 classes with the same properties? You only need one class that you will make an indeterminant number of instances of at runtime.
  • You should not store game state in an Activity. Activities are sort of ephemeral. If the user rotates the screen or takes a call or plugs in a keyboard, etc., the Activity instance will be destroyed and you'll lose your game data. The hard solution to this would be to make your game classes into Parcelables and use onSavedInstanceState to back it up and restore it after a configuration change. The easy solution is to just move those properties representing game state into a ViewModel.
  • Don't use Strings to represent state, if you can help it. That is fragile code where you can easily make a typo somewhere and the IDE won't be able to pre-emptively point out your error. An enum class would be much more fitting for representing your exitedFrom variable.

As for the question you're asking about, this is a natural fit for a MutableMap. A map holds entries by unique keys, and each key that is put in the map can hold one value (in this case, an instance of your Room class). Maps are designed to very efficiently look up values by key, but you can also efficiently iterate them to find something if you need to do it that way.

The keys should be members of a class that properly implements hashcode() and equals(), which is easily done automatically using a data class. In this case, the natural key type would be a class representing a coordinate. It is also important to choose a key class that is immutable to avoid potential bugs, so all the properties of this coordinate class should be vals.

So putting that all together, here's how I'd fix your code, with a couple of example functions that answer your question about finding rooms.

// Classes representing game state:

// This class is immutable and a data class so we can safely use it as a Map key.
// When we want to make changes, we use the copy() function to create
// a new instance of the class.
data class Coordinate(
    val x: Long = 0,
    val y: Long = 0,
    val z: Long = 0
) {

    // Convenience functions:
    fun movedByX(amount: Long) = copy(x = x   amount)
    fun movedByY(amount: Long) = copy(y = y   amount)
    fun movedByZ(amount: Long) = copy(z = z   amount)
}

class Room(val coordinate: Coordinate) {
    var exitT = true
    var exitB = true
    var exitL = true
    var exitR = true

    // I assume you'll add other properties such as whether it has a ladder
    // going up and/or down, etc.
)


// Enum to replace the String you were using for tracking movement direction
enum class Movement {
    Start, Left, Right, Top, Bottom, UpLadder, DownLadder
}
class GameActivity : AppCompatActivity() {

    // Read the Android docs about ViewModels to understand why we
    // instantiate it this way.
    val viewModel: GameViewModel by viewModels()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_game)

        fullscreen()

        // Set up the UI that draws stuff based on properties of the 
        // viewModel and calls functions of the viewModel when the user
        // clicks stuff.
    }
}
class GameViewModel: ViewModel() {
    var playerCoordinate = Coordinate()
    private var exitedFrom = Movement.Start

    // The Map containing all rooms that have been created in the game session.
    private val rooms = mutableMapOf<Coordinate, Room>()

    // This optional property is just a convenient shortcut.
    val currentRoom: Room
        get() = rooms.getValue(playerCoordinate)

    init {
        // Create the initial room by moving there
        move(Movement.Start)
    }

    fun move(movement: Movement) {
        exitedFrom = movement // I'm not sure if you really need this exitedFrom
                              // property but if you do, you can back it up here.
        playerCoordinate = when (movement) {
            Start -> Coordinate()
            Left -> playerCoordinate.moveX(-1L)
            Right -> playerCoordinate.moveX(1L)
            Bottom -> playerCoordinate.moveY(-1L)
            Top -> playerCoordinate.moveY(1L)
            DownLadder -> playerCoordinate.moveZ(-1L)
            UpLadder -> playerCoordinate.moveZ(1L)
        }

        // Create the room if it doesn't exist yet.
        if (!rooms.containsKey(playerCoordinate)) {
            val newRoom = Room(playerCoordinate).apply {
                // Customize the new room's properties here. Maybe randomized?
                // probably want to use the movement to ensure there is an exit
                // coming from the direction that was moved from so player
                // can backtrack.
            }
            rooms[playerCoordinate] = newRoom 
        }
    }

    fun resetGame() {
        rooms.clear()
        move(Movement.Start)
    }
}

CodePudding user response:

You have a few ways to search a list of objects, either by object attribute, object type, or a string representation of the object type. The last one is (I think) what you were trying to do (but it's probably not the most extensible code design choice).

Search by Object attribute

If you just want to search a list for an object based on an attribute of that object you can search for a match with some predicate using the find function on the list like this:

class Room(var name: String)

fun hasRoom(name: String, rooms: List<Room>): Boolean {
    return rooms.find { it.name == name } != null
}

val rooms = listOf(Room("A"), Room("B"), Room("C"))

hasRoom("A", rooms) // true
hasRoom("D", rooms) // false

You could also define an extension function for your room list for this or other common operations:

fun List<Room>.containsName(name: String): Boolean {
    return this.find { it.name == name } != null
}

which would let you call containsName directly on the room list, similar to what you are already trying to do:

val rooms = listOf(Room("A"), Room("B"), Room("C"))

rooms.containsName("A") // true
rooms.containsName("D") // false

Also, like the other answer pointed out you should definitely use a single class here with attributes, not a new class type for each coordinate. The coordinates can be attributes of the class, and the methods above for searching by attribute will still be applicable.

Search by Object type

If you really want to search a list of objects by object type and not an attribute of the object, you can still do that using an extension function as well:

class Room
class House
class Yacht
class Boat

inline fun <reified K> List<Any>.containsObj(): Boolean {
    return this.filterIsInstance<K>().isNotEmpty()
}

val rooms = listOf(Room(), House(), Yacht())

rooms.containsObj<House>() // true
rooms.containsObj<Boat>() // false

Search by Object type name

And if you really want to search based on a string name of the type, you can do that too

fun List<Any>.containsObjName(name: String): Boolean {
    return this.find { it::class.simpleName == name } != null
}

val rooms = listOf(Room(), House(), Yacht())

rooms.containsObjName("House") // true
rooms.containsObjName("Boat") // false
  • Related