1
$\begingroup$

Let me begin by saying I'm okay in python in general (and a half-decent programmer in some other languages), but I am very very new to python in Blender.

What I want to do

I have multiple objects all right on top of each other - say, a cube (1), a sphere (2), and a cone (3). When the game begins, only the cube is visible. The player can cycle through the objects by using the M and N keys, as in, when you cycle forward the cube becomes invisible and the sphere becomes visible. The player hits M to cycle forward, N to cycle backwards. When you hit the cone (the third object) you can't go forward anymore. Similarly, when you hit the cube (1st object) you can't go backwards anymore.

If you're curious, the application is I'm making a scientific visualization tool where you can step forward and backward through time while looking at datasets.

My hypothetical approach

I believe (but may be wrong) that I want to have some integer variable V that begins at 1; when you hit M it counts up; when you hit N it counts down; a statement checks if it goes below 1 or above 3 and corrects it back to 1 or 3 if so.

So here's a sort of code skeleton, which is incomplete and doesn't work yet, that gets at what I imagine the solution to look like:

V = 1 #initialize the variable V as 1 at game start

#Here would be some code to use the keyboard tap controllers M and N to
#increment/decrement the value of V, INCLUDING a check
#that if V < 1 you set it back to 1, and if V > 3 you set it to 3

#I think it would look something like this
#except I know I'm missing something:
if cont.sensors['m_key'].positive:
    V += 1
elif cont.sensors['n_key'].positive:
    V -= 1

if V < 1:
    V = 1

if V > 3:
    V = 3

#Now we check the value of V and set visibilities accordingly
#(which I also don't know how to do)
if V == 1:
    #set cube visible
    #set sphere invisible
    #set cone invisible
elif V == 2:
    #set cube invisible
    #set sphere visible
    #set cone invisible
elif V == 3:
    #set cube invisible
    #set sphere invisible
    #set cone visible

The tricky parts (for me)

All I know how to do so far in Python is scripts that only affect the parent object (the controller's owner) like so

import bge
cont = bge.logic.getCurrentController()
own = cont.owner

if cont.sensors['space_key'].positive:
    own.visible = not own.visible

This script, connected to a keyboard sensor called space_key with spacebar in tap mode, will toggle the visibility of the object in which the keyboard sensor resides. That doesn't help me handle multiple objects though.

I also don't know how to initialize V as 1 just at game start, and have it count up and down. Just for fun I tried this code fragment: import bge cont = bge.logic.getCurrentController() own = cont.owner

V = 1
print(V)

if cont.sensors['m_key'].positive:
    V += 1
    print(V)
elif cont.sensors['n_key'].positive:
    V -= 1
    print(V)

and the output looked like "121" when I hit m once, and "101" when I hit n once

So I guess my main questions are:

  1. How do I change the visibility of multiple objects (perhaps using the object name) from one script?
  2. Where should this script reside, along with the M and N controllers? Should it be attached to the cube, or to some empty object somewhere? Or is it going to have to be multiple scripts?
  3. If it has to be multiple scripts, how does the variable V get initialized properly, and how does it get globally recognized by all the different scripts?
$\endgroup$
  • $\begingroup$ I don't want to leave the very nice answer-givers hanging: I've been travelling and not had much time to try these out, but I'm now halfway done testing both python solutions out. It's taking a little time because like I said I'm still new to python in blender, and I'm trying to code it myself so I understand better, rather than copy and paste. Thank you so much for your helpful responses, and I should have some closure to this soon. $\endgroup$ – DrSandwich Dec 2 '15 at 21:46
3
$\begingroup$

Lets dig a bit deeper in the requirements and look how it fits your design (or find a better one):

  • You have a set of (ordered) objects = a list.
  • You want one and only one object be (lets say) "selected" at any moment
  • You want be able to select the next while deselecting all others.
    • Open Question: What should happen when the last object is already selected?

As there is always one object selected I suggest to use a token that indicates what object is selected.

This means you need a separate object as token. When you want to know what object is selected ... ask the token. If you want to select another object ... request it at the token.

This answers your question 2

Where should this script reside, along with the M and N controllers? Should it be attached to the cube, or to some empty object somewhere? Or is it going to have to be multiple scripts? -> the token does that.

Token

So lets have a closer look at the token.

  • It needs to know what object is selected.
    • We need a storage for a single game object at any time.
    • We need to be able to provide the selected object when asked
  • It should be able to select the next object in a list of objects
    • We need an ordered list of selectable objects (when performing this operations)

Lets look at a possible implementation of the token.

How you get your list of objects is up to you (e.g. you could look for objects with a specific property and order by property value). So I assume you implemented a function called getSelectables(). The following snippet contains a dummy implementation:

selection.py

import bge

#--- BGE callables
def next():
    if not allSensorsPositive():
        return

    deselectAll()

    next = findNext()
    select(next)

This is indeed not much. First (as any BGE code) check the sensors' status. Then deselect all selectable tokens. If you load them unselected, you can just deselect the current selected object deselect( getSelected() ).

Then find the next object and select it.

I hope you find this is what you want.

How to use it:

First you should select one object at startup. There is no special operation for that (you could if you want). So we simple call next():

Always sensor [no level triggering]
   ---> Python controller Module: "selection.next"

enter image description here

This will deselect all selectable objects and select the first one after loading the scene.

To select the next selectable object we call next() too:

Event sensor [no level triggering]
   ---> Python controller Module: "selection.next"

enter image description here

Please note, next() does not do anything when not all sensors are positive. Therefore you can't connect both sensors to the same controller.

Why is it that way? Imagine you want to deactivate the ability to call next(). In this way you can connect another sensor that controls the activation:

enter image description here

You tried that? I hope you noticed the code is not complete. If you want you can try to implement your own version. But here are the details that can help you.

selection.py continued ...

#--- internals

INTERNAL_PROPERTY_SELECTED_OBJECT = "_internal.selectedObject"

def deselect(object):
    object.visible = False

def select(object):
    getOwner()[INTERNAL_PROPERTY_SELECTED_OBJECT] = object
    object.visible = True

def deselectAll():
    for selectable in getSelectables():
        deselect(selectable)

def findNext():
    selectables = getSelectables()
    selected = getSelected()
    try:
        nextIndex = selectables.index(selected) + 1
    except ValueError:
        nextIndex = 0

    return selectables[nextIndex % len(selectables)]

def getSelected():
    return getOwner().get(INTERNAL_PROPERTY_SELECTED_OBJECT)

def getSelectables():
    'Implement your own object list'
    objects = bge.logic.getCurrentScene().objects
    return [objects[name] for name in 
            ["Cube", "Sphere", "Cylinder"] ]

#--- internal utilities
def allSensorsPositive():
    for sensor in bge.logic.getCurrentController().sensors:
        if not sensor.positive:
            return False
    return True


def getOwner():
    return bge.logic.getCurrentController().owner

Here you see that selection/deselection actively changes the involved objects (setting visibility). When selecting the object running this code will store the selected object in a property replacing any previous selection.

Finding the next selectable object is simply looking at the list and grab the next one by index. If the current selection can't be found, it the first one gets picked. If the last one is selected ... it starts with the first one again nextIndex % len(selectables). If you do not want that you change that code to min(nextIndex, len(selectables)-1).

As said earlier you should implement your own version on getSelectables().

The utility functions are often used and would live happily in a common module. They are here to complete this snippet for you.

As homework you might want to implement a previous() function.

I hope it helps you

Edit: I forgot to answer your other questions:

How do I change the visibility of multiple objects (perhaps using the object name) from one script?

Either each object doe it by itself (e.g. via Visibility actuator), or you let the object do it by itself via KX_GameObject.visible (see snippet above)

If it has to be multiple scripts, how does the variable V get initialized properly, and how does it get globally recognized by all the different scripts?

Variables are always local to the code. You could create a module variable that is a singleton with the module. The above solution uses a property of the selecting object. This means you can have different selecting objects each with own selection (I guess this is not what you want).

If you want to know if an object is selected or not you have to ask the selecting object. Alternative, you can check if the selectable object is visible or not.

$\endgroup$
  • $\begingroup$ Thank you so much. I implemented this and it took some time to work out some bugs. I now have working next and previous functions. The main change I had to make was, it only works for me if the Always sensor is set to "Tap" and both pulse modes are disabled. Also, I implemented your INTERNAL_PROPERTY_SELECTED_OBJECT thing, but I don't 100% understand how it works. The program doesn't work without it though. I get the concept: when I select an object, I mark it as selected using this property so that I can later look up who is selected. But I don't quite understand the implementation. $\endgroup$ – DrSandwich Dec 3 '15 at 16:18
  • $\begingroup$ It simply stores data in a property of the game object. It is labeled "internal" as it is not supposed to be set via GUI or for communication with other aspects of your game. - in this specific case, it holds a reference to the currently selected object. $\endgroup$ – Monster Dec 4 '15 at 10:24
1
$\begingroup$

I know you didn't ask for it, but i'm going to throw the logic brick answer out there just in case anyone who hates python with a passion comes along later with the same question.

here is the recommended option:

Use the "Replace Mesh" option for "Edit Object" Actuator

you will need to make sure you named the meshes of the three different objects, not just the object name. This can be done under the "Mesh" panel of the properties editor.

Change the mesh name

next, you need to add a new mesh object on another layer. It really doesn't matter what shape it is because you will never have to see it. I used Suzanne since she isn't already being used in this example.

You will need to add an Integer variable to the suzanne. Please name it prop if it is not named that by default. Here is a list of sensors you will need:

  • right arrow (keyboard sensor)
  • left arrow (keyboard sensor)
  • prop > 1 (property sensor)
  • prop < 3 (property sensor)
  • prop = 1 (property sensor)
  • prop = 2 (property sensor)
  • prop = 3 (property sensor)

We will be changing the variable to tell the suzanne object what mesh she should change to. This system is easy to change and because it uses a variable rather than direct wiring, it can slow your system up to 80% less.

Now you need some Actuators:

  • prop+1 (property actuator)
  • prop-1 (property actuator)
  • Replace_With_Cube (edit object actuator set to "Replace mesh")
  • Replace_With_Ball (edit object actuator set to "Replace mesh")
  • Replace_With_Cone (edit object actuator set to "Replace mesh")

and at the end of the day your logic looks like this:

Here is the finished logic

$\endgroup$
1
$\begingroup$

I think I can do it shorter than Monster, you be the judge how good it is:

  1. If it has to be multiple scripts, how does the variable V get initialized properly, and how does it get globally recognized by all the different scripts?

Don't do that. You don't even want multiple objects executing the same script.

  1. Where should this script reside, along with the M and N controllers? Should it be attached to the cube, or to some empty object somewhere? Or is it going to have to be multiple scripts?

What you want is to set up a dummy object to control things for you, you give that your keyboard sensor and a python controller and nothing else.

Now to initialize things you have to remember that the game logic main loop runs through the script every frame so simply initializing at 1

V=1

Just sets your scroll status to 1 every single frame, even if you hit a button the frame before. What you need is to check the space that's persistent and because of decisions made in the past the only place you can find that is in objects.

So you need to do this in your script:

if not "init" in DummyEmpty:
    DummyEmpty["init"]==True
    DummyEmpty["V"]=0

This creates a property while the game is running and you now have a persistent variable you can check.

Now you get your list (by looking up the object names in scene.objects)

l = [ob_a,ob_b,ob_c]

and you set the visibility

c=0
m=len(l)
while c < l:
    ob = l[c]
    if c==cont.owner["V"]:
        ob.visible=True
    else:
        ob.visible=False
    c+=1

How do I change the visibility of multiple objects (perhaps using the object name) from one script?

I should add that your idea about hitting a key and increasing your counter is absolutely correct and the way I would do it too, so I didn't include that here.

This should solve your particular problem, the stuff I didn't cover is not really related.

tl;dr: do something like this:

import bpy

# edit: you have to tell the engine what "DummyEmpty" is of course
# if you're running the script from the Empty that would look like this:

cont=bge.logic.getCurrentController()
DummyEmpty=cont.owner

# end edit

#this only runs once
if not "init" in DummyEmpty:
    DummyEmpty["init"]=True
    DummyEmpty["V"]=0

    scene=bge.logic.getCurrentScene()
    l=[]
    for ob in scene.objects:
        if "key" in ob.name: #like "key" in an object named "object1key"
           l.append(ob)

    #l = [ob_a,ob_b,ob_c] should look something like this
    #save it
    DummyEmpty["l"]=l

cont=bge.logic.getCurrentController()
owner=cont.owner

l=owner["l"]

c=0
m=len(l)
while c < l:
    ob = l[c]
    if c==cont.owner["V"]:
        ob.visible=True
    else:
        ob.visible=False
    c+=1
$\endgroup$
  • $\begingroup$ Thank you. I tried this, and I can't quite get it to work! Even though @Monster's answer worked well, I still want to try yours - I am very interested in how to have a variable initialize at a value, and be tracked consistently throughout gameplay. Basically, when I do if not "init" in DummyEmpty: as you wrote, it complains that DummyEmpty doesn't exist. I tried getting around this via try: DummyEmpty except ValueError: [and then the rest] and it still re-initializes the variable DummyEmpty to 0 every time! I'll keep trying, but if you have any thoughts about this I'd be grateful. $\endgroup$ – DrSandwich Dec 3 '15 at 16:26
  • $\begingroup$ Yes sorry I was a bit imprecise, you have to have your usual initialization. DummyEmpty should be an empty object in your scene and you have to tell the game engine that that's the case too. So use the usual method to get this gameobject, either by using the script with the empty and going over the the controller or by doing it somewhere else and going over scene.objects . $\endgroup$ – B Newmark Dec 21 '15 at 10:12

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.