How do I change models with a menu?

I’m trying to create an empty space (a full screen default window set to black) that only needs to display one 3d model at a time chosen by a user from a menu. I have tried using the DirectOptionMenu, and have been able to get the menu to load models how I want, but I can’t figure out a way to unload the previous model when a new selection is made from the menu. I’ve run into two problems: (1) while loader.load_model works, for some reason loader.unload_model does not and (2) because the options are user selected I can’t predict which menu option they will choose and therefore can’t put any specific model into loader.unload_model(specificModel). I’ve tried creating something like:

currentModel = ()
if arg == ‘example1’
loader.unload_model(currentModel)
currentModel = example1
model = loader.load_model(currentModel)
model.reparent_to(render)

but it didn’t solve my problem. Does anyone know how to unload and load models with a single menu selection? Thanks y’all!

(edit: when I posted this it took out the indentations from my example. Assume I indented correctly and that is not part of the problem.)

Perhaps this example will help how to do it.

For this purpose, unless you want to do more than just remove the model from the scene, I don’t think that you need use “unload_model”. Instead, you should be able to simply store your current model in a variable and then use “remove_node” to, well, remove it–like so:

In your initialisation:

# Create a variable in which the curent model is to be stored.
# Set it to initially have the value "None", to indicate that no
# model is yet loaded.
self.currentModel = None

And in your menu-code:

# Check whether we already have a model... (so that we don't try to call a 
# method on a non-existent object)
if self.currentModel is not None:
    # And if we do have an old model, remove it
    self.currentModel.remove_node()

# And now, load a new model and store it in the same variable,
# so that it might be removed later.
self.currentModel = loader.load_model(some_new_model_name)

This, too, is covered by the above suggestion: since it stores the current model in a variable, you have access to that model in order to remove it.

For future reference, code-tags should allow you to preserve indentation (and even have some code-highlighting). At the top of the posting text-box, there should be a row of buttons. One of these looks a bit like the Python logo, or like a “+”-sign. Clicking on this should produce the forum’s code-tags, looking like so:
```python
indent preformatted text by 4 spaces
```
Simply replace the line that starts with “indent” with your code, and you should see it appear properly formatted!

(And you can, of course, just type the code-tags in manually, if you prefer.

@serega-kkz I’ll have to spend some time digging into this method. It might end up being easier to think of my models as changing levels rather than changing characters in a level after all. Thanks for the link!

@Thaumaturge I tried a similar method but I used

if currentModel:

as in the shorthand of “if currentModel == True”. I did not use the term “None”, though, and I didn’t use “self” either, so those factors might have been my shortfall. I also used both “loader.unload_model(currentModel)” and “currentModel.remove_node()” one after the other, which I don’t think would interfere with each other but I guess it wasn’t helping the situation. I’ll have to give it a go with your version and see if that straightens it out. Thanks!

The main thing to beware of when not using “self.” is scope–that is, the region in which a variable exists, and outside of which it is “thrown away”.

Specifically, if “currentModel” (without “self.” or some similar thing) is not a global variable, and if this code happens in a method, then the “currentModel” that you declare at the start of the method will I believe not be the one that you used the last time that it was called; it’s a new “currentModel” entirely each time. As a result, it doesn’t contain the model that you created in that previous call to the method, and so removing or unloading the node won’t work.

This should work, I do think–if not for the above-mentioned issue of scope, of course.

@Thaumaturge That explanation of “self” makes a lot of sense; thanks for that. I’ve run into another problem now, though. If I add “self” to the standard form of the DirectOptionMenu, how do I get the item selection to perform a task now? Before it was just “def itemSel(arg):” and I could use “if arg == ‘item1’:” and then add a task for it to perform. Now if I change it to “def itemSel(self, arg):” arg doesn’t do anything (The error code is “TypeError: itemSel() missing 1 required positional argument: ‘arg’.”) A rough version (still mostly cut and paste from the manual) is below:

   class Objects:
        def artifacts(self):
            self.currentModel = None
            self.artifact1 = 'NAPot/NAPot.glb'
            self.artifact2 = 'Clovis.obj'

        def itemSel(self, arg):
            # Add some text
            if arg == 'item1':
                if self.currentModel is not None:
                    self.currentModel.remove_node()
                self.currentModel = loader.load_model(self.artifact1)
                self.currentModel.reparent_to(render)
            if arg == 'item2':
                if self.currentModel is not None:
                    self.currentModel.remove_node()
                self.currentModel = loader.load_model(self.artifact2)
                self.currentModel.reparent_to(render)
            output = ""
            textObject = OnscreenText(text=output, pos=(0.95, -0.95), scale=0.07,
                                      fg=(1, 0.5, 0.5, 1), align=TextNode.ACenter,
                                      mayChange=1)
            output = "Item Selected is: " + arg
            textObject.setText(output)

        # Create a frame
        menu = DirectOptionMenu(text="options", scale=0.1, command=itemSel,
                                    items=["item1", "item2", "item3"], initialitem=2,
                                    highlightColor=(0.65, 0.65, 0.65, 1))

When passing your method to the DirectOptionMenu, you simply include “self.” before it. That is, something like this:

menu = DirectOptionMenu(text="options",
                        scale=0.1,
                        command=self.itemSel, ### Note this line
                        items=["item1", "item2", "item3"],
                        initialitem=2,
                        highlightColor=(0.65, 0.65, 0.65, 1))

Honestly, I’m a little surprised that the code runs at all! With “itemSel” declared in a class and with “self.” not included, I would expect the code to fail with an error indicating that it can’t find something called “itemSel”.

However, the error would seem to suggest that “itemSel” is being called, but lacking the attached “self.”, is being treated as a method that’s not bound to an instance of a class. As a result, the reference to that instance isn’t being automatically passed in to the initial “self” parameter, as usually happens. This means that the argument provided by DirectOptionMenu (which would otherwise end up in the “arg” parameter) is ending up in the “self” parameter–and thus leaving the method short by one parameter.

In order to add “self.” to “command = itemSel” I seem to have to indent the menu script so that it has a reference for “self.” But if I do that, the menu doesn’t appear when the program is run. If I indent the menu into the method so that it has a reference for self do I need to then somehow call the menu elsewhere in my script to get it to display? The Objects class I created is nested into a class called App that already has self.Objects() in its

def __init__(self)

method, which I thought would be enough to call the menu from inside the itemSel method.

If you want to instantiate your menu within the “Objects” class, I’d suggest giving that class an “__init__” method and then placing the menu-construction code in that. By doing so, that code should be run when the “Objects” class is instantiated within the “App” class, allowing your menu to be made.

If I’m understanding correctly, it’s a matter of when the relevant section of code is run: “itemSel” is, after all, only run when an item is selected from the menu. As a result, it won’t be run if the menu hasn’t been constructed–and if the menu is only constructed within it…

(You could, of course, call the method yourself. But then you’d end up running all the other code in the method, which likely isn’t relevant to the task of constructing the menu.)

Yep, it finally works the way I want it to. This definitely added to my one semester of intro python’s worth of knowledge. Thanks for the help!

1 Like

It’s my pleasure, and I’m glad if I’ve helped! :slight_smile:

If you want a bit more practise with Python and with Panda, I have a “beginner’s tutorial” that might be of use to you. It does assume some degree of familiarity with Python, but as you do already know somewhat, the process of going through the tutorial and of seeing its code develop might help to build on your current knowledge.

If you’re interested, you should find it here:

1 Like