BGE Python - proposal / discussion.

As requested in this thread, here are my thoughts for BGE Python.


################
#
# Game            class MyGame(Game):
#                    # Game-wide functions and data
################

    ################
    #
    # Scene            class MyScene(Scene):
    #                    # Scene-wide functions and data
    ################

        ################
        #
        # GameObject        class MyGameObject(GameObject):
        #                    # GameObject logic/events/behavior
        ################

Assume there is a place in blender where scripts for each of those objects could be set, and where those scripts could extend those objects with our own methods/data - then our newly defined objects assume those roles.

If no script is assigned, the default base objects (Game/Scene/Object) are used, with all the usual functionality they already provide. So, if you don’t feel the need to put some additional functionality on Game or Scene, you don’t have to, but the option is there.

The GameObject is usually what you’ll want to extend, and you’ll also require a convenient way to trigger your newly added methods on some specific event, or a collection of different events.

I think that event callbacks are probably the best way to do so:


class Bomb(GameObject):


    def __init__(self):

        self.EFFECT_RADIUS = 10
        self.POWER = 5

        # Sensors:
        self.collision = CollisionSensor()
        self.always = AlwaysSensor()

        # Timer(start_count=0, forward=True)
        self.countdown_timer = Timer(3, False)

        # Whenever this object collides with something, 
        # call self.collisionHandle, and pass the collision sensor
        self.addCallBack(self.collision, self.collisionHandle)


    def collisionHandle(self, sensor):
  
        if sensor.hitObject.name == "Floor":
            
            # There is no need run this function anymore:
            self.removeCallBack(self.collision, self.collisionHandle)

            self.countdown_timer.start()
            # But now we want to call this one, on every single tick
            self.addCallBack(self.always, self.countDown)


    def countDown(self, sensor):

        if self.countdown_timer <= 0:
            
            detonate()
            
        
    def detonate(self):

        for obj in Scene.objects:
            hit_obj, vec = self.rayTo(obj.position)
            if hit_obj and vec.magnitude <= self.EFFECT_RADIUS:
                vec.magnitude = self.POWER
                hit_obj.setLinearVelocity(vec)
        
        smoke = Smoke(this.position)
        Scene.addObject(smoke)

        Scene.removeObject(self) # Implicit removal of all active callbacks

The definition of the “Smoke” object that’s being added:


class Smoke(GameObject):


    def __init__(self, position):

        self.position = position

        # Sensor
        self.always = AlwaysSensor()
        
        self.anim_timer = Timer(3, False)

        self.anim_timer.start()
        self.addCallBack(self.always, self.fadeAlpha)


    def fadeAlpha(self, sensor):
        
        if self.anim_timer > 0:
            self.material.alpha = self.anim_timer/3
        else:
            Scene.removeObject(self)

If we wanted a smoke object that would additionally rise:


class RisingSmoke(Smoke):

    def __init__(self, position):
        Smoke.__init__(self, position)

        self.addCallBack(self.always, rise)


    def rise(self, sensor):

        lin_v = Vector([0, 0, 0.1])
        self.setLinearVelocity(lin_v)

Then you could simply add RisingSmoke instead of Smoke:

Scene.addObject(RisingSmoke(some_position_vec))

Or, how about a bomb with “last legs”:


class Kamikaze(Bomb):

    def __init__(self):
        Bomb.__init__(self)

        # Sensor
        self.keyboard = KeyboardSensor()

        self.SPEED = 1

        self.addCallBack(self.keyboard, keyHandle)

   
    def keyHandle(self, sensor):
        
        force = Vector([0,0,0])

        force.x = sensor.keyDown(Keys.RIGHTARROW) - sensor.keyDown(Keys.LEFTARROW)
        force.y = sensor.keyDown(Keys.UPARROW) - sensor.keyDown(Keys.DOWNARROW)

        force.magnitude = self.SPEED

        self.applyForce(force)

Multiple events could be set to a single function:


self.addCallBack(self.keyboard, myHandle)
self.addCallBack(self.collison, myHandle)

def myHandle(self, sensor):
    if sensor.type == "Keyboard":
        # your key handling stuff
    elif sensor.type == "Collision":
        # your collision handling stuff

But, you might as well have two functions in that case; something like this would be more usefull:


self.addCallBack([self.keyboard, self.collision], myHandle)

# This function only runs if both are active
def myHandle(self, sensor_list):
    key_sen = event_list[0]
    col_sen = event_list[1]

    if key_sen.keyDown(Keys.SPACE) and col_sen.hitObject.name == "Target":
        # do something

Performance wise, this could be well tuned, becuase each function runs only when pre-determined conditions are true, and ideally it would all be handled very efficiently by some underlying event system. It’s all probably easier said than done, but really, if you think about it, the whole thing works like an implicit state machine - the currently registered callbacks represent the current active state.

And for the people who just want to do it their own way:


class MyThing(GameObject):

    def __init__(self):

        self.always = AlwaysSensor()
        self.keyboard = KeyboardSensor()
        self.mouse_move = MouseMoveSensor()
        self.collision = CollisionSensor()

        self.addCallBack(self.always, main)


    def main(self, sensor):
        
        if self.keyboard.keyDown(Key.SPACE):
             # whatever

        if self.mouse.positive:
             x = self.mouse.x
             y = self.mosue.y
             # do something with x and y

        if self.collision.positive:
             # and so on

Now, just to make it clear: I’m not saying this is “the way” it needs to be done in the BGE - this is simply a starting point for a discussion - but really, I think we can all agree that the current state of Python in the BGE is abysmal, and that either way, something needs to be done about that.

So, what are your thoughts - would you like to see something like this, do you think something else would work better?

Feel free to share.

hmmm, I have a small suggestion to your proposal.
Comming from JAVA RCP Programming, I see it to let the sensors handle it’s events by themself, rather than use a GameObject with callbacks.

There are sensors

  • related with objects, like collision or mouse over sensors and
  • independend from an object, like mouse or keyboardsensors.

The sensors related to GameObjects, would need an GameObject for construction. The GameObject can, but does not need to be a listener of this sensor.

The independend sensor types do not need a GameObject to work. Some sensors could exist as singleton (keyboard), while other could exist in multiple instances (timer).

So you could add the sensors if they are needed. Then register GameObjects or any other type as listener to the sensor. That means one sensor can notify multiple objects, while the sensor can be independend from the listening GameObject.

example:


class Bomb(GameObject):


    def __init__(self):

        self.EFFECT_RADIUS = 10
        self.POWER = 5

        # Sensors:
        self.collision = CollisionSensor(self) # new instance of the Collsionsensor
        self.always = AlwaysSensor.instance() # access to an singleton of a sensor
        self.init = InitSensor(self) # Do we need that? this is init already!


        # Timer(start_count=0, forward=True)
        self.countdown_timer = TimerSensor(3, False)

        # Whenever this object collides with something, 
        # call self.collisionHandle, and pass the collision sensor
        # instead of self.addCallBack(self.collision, self.collisionHandle) do
        self.collision.addCollisionListener(self.collisionHandle )

        self.countdown_timer.addTimeoutListener(self.onTimeout) # listener to a instance
        TimerSensor(2,True).addTimeoutListener(self.onAnotherTimout) # listener on a new listener instance
        KeyboardSensor.addKeyPressListener(self.onKeyPress) # listener to a singleton
   
    def collisionHandle(self, sensor???is this necessary???):
        #if sensor.hitObject.name == "Floor":           
        if self.collision.hitObject.name =="Floor": # the reference to the sensor is already stored
            # There is no need run this function anymore:
            #self.removeCallBack(self.collision, self.collisionHan...
            self.collision.removeListener(self)
            # or
            del( self.collision ) # should remove the sensor
    


What do you think?

[Edit]:
Beside creating new objects and sensors, it would be possible to create (python) wrapper classes already demonstrating this approach. Your approach with callbacks could be realized with wrapper classes as well.

With such a demo, we could see if this is acceptable for BGE users.

Would the CollisionSensor (and possibly other sensors) need more than one listener? I can’t seem to find a reason why an object would need more than one collision method? Sorry if that sounds stupid :slight_smile:

This is interesting and I’ll sure to be following this thread.

The event call backs should be done in such a way that they don’t only accept sensors.

# self.addCallback(callable, boolean, data)
self.addCallback(self.collisionHandle, self.collision.positive, self.collision)

This allows for the removal of the keyboard sensor (and others) for one:

keyboard = bge.logic.keyboard

w_key = [bge.events.WKEY, bge.logic.KX_INPUT_ACTIVE] in keyboard.events

self.addCallback(self.wHandle, w_key)

And the addition of persistent events:

self.addCallback(self.main, True)

The way you demonstrate the adding of objects may not work. If an object is added using just a class, than that class would need to have all the data (mesh, physics, ect) already defined in it somewhere. Which would be rather laboursome. Entering the object through the UI and adding the object through the normal way (either though its name, scene.inactive_objects or dynamically loading it) is the best way to go rather than using the class.


In regards to Monster’s point about object independent sensors, possibly registering event call backs on scenes may be the solution.

How would a visual logic system (logic bricks or nodal logic) be built upon this?

@Hendore: Why not? This allows to trigger multiple tasks with the same collision sensor.

E.g. usually the object with the collision sensor is listener. But a collison counter (text object) might be listener as well. Please keep in mind you could have even have the same listener with different notify functions registered. Similar to multiple controllers on one sensor.

A lot can be done with wrapped classes (I’m doing this in my current project), but I figured it would be nice if a subclass could be done, especially if automatically assigned. I started doing some poking, and I found that subclassing already works. Here is a quick script to demonstrate this:


# The following code already works in 2.5

import bge # New top level module in 2.5

class MyGameObject(bge.types.KX_GameObject):
	
	def __init__(self, gameobject):		
		bge.types.KX_GameObject.__init__(gameobject)
		
		self['class'] = self
	
	def main(self):
		if (bge.events.SPACEKEY, bge.logic.KX_INPUT_ACTIVE) in bge.logic.keyboard.events:
			self.applyRotation((0, 0, .05), True)
		
ob = bge.logic.getCurrentController().owner

if "class" in ob:
	ob['class'].main()
else:
	MyGameObject(ob)

The only logic bricks on the object are an always sensor hooked up to a python controller. So, I think the next step is to be able to assign a class to a game object like in andrew-101’s example. I’m thinking the class’s main() method would get run every frame.

Thoughts?

Cheers,
Moguri

@Monster

Yes, a singleton for things like the always sensor would probably be a good idea.

Regarding the collision sensor: I was thinking that multiple instances could allow for something like: collision = CollisionSensor(“ObjectName”).

That particular sensor would only trigger a call-back on a specific collision, and you wouldn’t have to do the checking in the called function.

Also, there may be some other sensor options that someone might want to enable.

@andrew-101

I never planned to define all the data on the object “by code” - as I said: “a place in blender where scripts for each of those objects could be set, and where those scripts could extend those objects with our own methods/data”.

You and I are on the same page in that regard (I think).

However, I don’t see the benefit in your addition to the callback function: I mean, you’re getting rid of the keyboard sensor (and maybe others), but you’re now responsible for actually checking when the event occurred; the whole point of having an event callback system is so you don’t have to do the checking yourself.

Also, you can just do this, without crowding addCallBack:


###  Init ###
self.addCallBack(self.always_sensor, self.alwaysHandler)
##########

self.alwaysHandler(self,  event):

    keyboard = bge.logic.keyboard

    if  [bge.events.WKEY, bge.logic.KX_INPUT_ACTIVE] in keyboard.events:
         self.wDown()

self.wDown(self):
    # blah, blah

@Moguri

But isn’t that just another wrapper? That’s exactly the kind of thing we want to avoid.

Also, if calling main on every frame is the only thing that can actually be done, in the event department, then the returns on that “development” (getting rid of the always sensor) are inconsequential -> I’ll just add the always sensor, and run my own functions.

The key notion here is this: Along with inheriting from some specific game object, our class has to become that very game object, as far as the engine is concerned.

So, there needs to be some underlying blender process that will do something like the following when we press P:


std::vector<KX_GameObject*>  game_objects;

for (int i = 0; i < BLENDER_OBJECT_COUNT; i++){
    KX_GameObject* gobj;
    if (blender_objects[i]->script){
        gobj =  createExtendedGameObject(blender_objects[i]);
    }else{
        gobj =  createGameObject(blender_objects[i]);
    }
    game_objects.push_back(gobj);
}

Then when we get our object from the global objects list, we can treat it like the original object: object.applyRotation(…), object.setLinearVelocity(…), object.ourCustomMethod(…), object.our_custom_prop … etc.

@Social
I was more making an observation than suggesting a solution.

Last night I played with getting custom classes going in the BGE. Here are my results:

And a Windows build on GraphicAll to play with it:

This is just a start, and only currently supports a main method and no callbacks. Feedback is welcome.

Cheers,
Moguri

Your idea is going to revolutionize the game engine.

My idea:
That we have specific events as well as the basic sensors.

Keyboard events:


self.setCallback("KeyPress", self.handleKeys)

self.setCallback("a", self.handleAKey) # set a call back for the "a" key
-or-
self.setCallback("akey", self.handleAKey)

Mouse events:

self.setCallback("click", self.handleClickWithNoMouseOver)
self.setCallback("mouseover-click", self.handleClickWithMouseOver)

I was thinking along the lines of the syntax from the Python Tkinter bind function:

<modifier-type-detail>
(We wouldn’t have to use triangle brackets if you don’t want to.)

Where modifier can be, for example:

  • Double (A double tap on either a keyboard key or mouse button)
  • Alt, Shift, Control (The standard modifier keys)

type is the type of event, for example:

  • Any key (Where you can just put any printable keyboard key (e.g. <g>, <F1>))
  • Button (A mouse button (See the next section))

detail is an extra detail for the event, for example:

  • 1, 2, 3, 4, 5 (Representing mouse buttons, respectively, Left button, middle button, right button, scroll up, scroll down)

type would be the most important one. Any of the others could be left out at anytime.

Also, like Tkinter, when the callback is called, it would be given an Event object that would give certain info on the event. Such as mouse coordinates (event.x/y; mouse event), keyboard key (event.key; keypress event)

This would also make it possible to assign Protocols.

Not only will the code become more readable, it will also allow for more freedom on the user’s side; to mix and match events.

We should still keep event constants like self.collision and such.

-Sunjay03

@Moguri

Yea, that looks really good.

If I were to get that object from the global objects list, would I get that class, and would I be able to call object.main()?

If so, that’s going to be really helpful.

I would be glad to test it (on Linux), if you can point me to the git/svn branch that hosts these changes (I assume this is not in the blender trunk already…*checks…doesn’t look like it).

@Sunjay03

Well, yea, that goes back to preserving sensors as instances, and using them to define events, because you can have a mouse sensor instance that can be configured to callback only on specific sub-events, and so on.

For protocols: I don’t know. It’s something I need to research a bit, but at a quick glance, the distinction between protocol and callback doesn’t seem that apparent to me.

I think our time would be better spent on trying to figure out the best approach for user defined events.

For example: creating a list of conditions, and then giving that to the event system as something to track.

When you think about it, a lot of the code you write is just for checking if specific conditions are true, before you actually do something that effects the scene.

It would be nice if that checking could be done by the underlying system, rather than the user directly - it would just make the code crystal clear -> “This event is defined by these conditions, and when it happens, this function runs”.

Well, maybe that’s too much to ask.

I agree completely.

Protocols are for things like catching higher level program events. Like when the window gets/loses focus, and when the window is closed.

-Sunjay03

@Social

Here’s a diff of my SVN working copy.

Some notes:

  • I’ve implemented a collision callback (currently called on_collision and sends the object that the current object collided with).
  • The python_class attribute of KX_GameObject can be odd to use (setting the class invalidates the KX_GameObject), and I might remove it. I used it to initially get started before I had the ui.
  • Only external py files work, I currently don’t have support for datablocks.
  • This is all still very experimental, and probably has lots of bugs. :slight_smile:

http://www.pasteall.org/14134/diff

I think you would actually get your custom class if you grabbed that object from the object list, but I haven’t tested it yet.

Cheers,
Moguri

Moguri, this works very well! The only issue for me is you can’t yet get error handling from inside the class.

I think you would actually get your custom class if you grabbed that object from the object list, but I haven’t tested it yet.

You do, but like you said only main() is working (You can see other bound methods, just not call them)

Right now the only way to implement your own python scripts in the BGE is through python controllers. Since users have been demanding more control over the BGE through python, the API for python controllers has become somewhat convoluted. In 2.5 there is only 1 sensor (That I can think of) that holds data python doesn’t have direct access to, yet python is still tied directly into the logic system. This makes the entire python API seem limited, and hacky, which isn’t ideal.

I propose we split the API into two parts.

Python controllers would be limited to acting as a sort of advanced expression controller, with no access to anything but linked sensors, actuators and object properties, not even regular python modules. This would clean the python controller API out and give it a specific use.

Then externally we could implement the ideas expressed in this thread as a separate API, giving users full control over the BGE in python. I would prefer it if the two APIs were completely separate, giving this proposed API no access to an objects logic bricks, unless completely necessary.

This would allow Logic Bricks to act as a sort of visual way of constructing callback functions, while easing the learning curve between logic bricks and python with the python controller API.

hmm…

I still can’t see the point why you want to set up a callback on a GameObject. The events belong to the sensors not the game objects. I think the callback should be on the sensor.

I’m a noob, but I have found this to be true - when I went to make a skid steer vehicle with six wheels it was easy; just parent the wheels to the body and assign keys to move the body, but this is a tank, with a turret and barrel, and when I went to assign the turret and barrel to the mouse I found it couldn’t be done - there is no way to assign the x,y movements of the mouse with just the datablocks. (I did do it, by using wsda for the tank, and the arrow keys for the turret and barrel - the only other way is with a mouselook.py script that I’m still having trouble with. Think about it; the mouse must move the turret and the barrel left and right, but it can only move the barrel up and down. It is a lot of extra work, and that is with 2.49b.)

Isn’t there a GSoC project going on right now to work on the BGE’s Python API?

Sorry; didn’t mean to wonder into a running dialog, of stuff I still don’t know too much about, but as a new user, I would prefer to be able to do anything, just about, with the radio buttons that I can do with the script, and vice versa.

@Moguri

On Linux (and I believe other *nix systems) /home/user is the default search path, so you have to account for that.

I made it work with the following:


--- a/source/gameengine/Converter/BL_BlenderDataConversion.cpp
+++ b/source/gameengine/Converter/BL_BlenderDataConversion.cpp
@@ -2630,6 +2630,10 @@ void BL_ConvertBlenderObjects(struct Main* maggie,
        kxscene-&gt;SetWorldInfo(worldinfo);
 
 #ifndef DISABLE_PYTHON
+       // Ammend module search path
+       PyRun_SimpleString("import os, sys 
");
+       PyRun_SimpleString("sys.path.append(os.getcwd()) 
");
+
        // convert Python class
        for (i=0; i&lt;logicbrick_conversionlist-&gt;GetCount(); i++)
        {

That’s a quick fix, but I think we could use the same system that python controllers (set to run modules) use when they look around for valid scripts (either in the current working directory, or the internal text-buffer).

… I’ll continue testing this.

+1

This is the best idea so far; it would help on so many different levels.

Thanks gomer.

Think of the sensor object as something that defines an event, rather than something that listens for one.

It’s just an object you pass to a function that carries certain event attributes (mouse sensor would carry current x, y position).

The object, and more specifically, the function should do the listening.

… or at least, that’s the theory. Maybe it would be better to completely dump sensors and use specific event objects instead? I think that would probably be better, but I used the idea of a sensor because it’s something we’re all familiar with.

What about those who already use a lot of python controllers to call complex scripts that use different modules already, would we have to completely rewrite our games as we would have to shift things to the advanced API to control everything through Python?

We already had to update our scripts for 2.49, and make more changes for the current builds of 2.5, surely if this requires taking all the logic apart in current games and putting it back together using Python only it would be a headache for those who have games with complex logic systems already established. Not to mention you would have to learn a bunch of new Python concepts before you can even start to rewrite.