Hi, tried to write a Conway life in 3D
everything worked so far, but now I wanted to implement a edit feature. I implemented it so that there is a small cube at each grid position. When clicked, it should add or remove a cell. This is not fully implemented yet, but the collission detection works already. (line 186, the hide() function in 187 works)
For that the actual cube under the mouse should be highlighted. But setColor doesn’t work for some reason.
After I encountered that problem I added another setColor when the objects are created, but even there it doesn’t work (lines 132 and 140)
Later there will be a button to show/hide those editmarkers
Please ignore other details, the thing isn’t finished yet, but I need the highlighting functionality
To make the thing run you would need two simple models. a cube (cube.egg) and a smaller cube simple_cube.egg)
I created both with blender + chicken
#!/usr/bin/python
# -*- coding: utf-8 -*-
# Import section
import direct.directbase.DirectStart
from direct.showbase.DirectObject import DirectObject
from pandac.PandaModules import *
from direct.gui.DirectGui import *
from direct.task.Task import Task
from direct.interval.IntervalGlobal import *
import sys
class World(DirectObject):
# ruleset
rule_set = {"new_min" : 3, "new_max" : 6, "survive_min" : 4, "survive_max" : 6}
wrap_walls = 0
# grid size
size_x = 11
size_y = 11
size_z = 11
size_plane = size_x * size_y
size_total = size_x * size_y * size_z
# data array
models = [[[0]*size_z for x in xrange(size_y)] for y in xrange(size_x)]
edit_markers = [[[0]*size_z for x in xrange(size_y)] for y in xrange(size_x)]
collider_spheres = [[[0]*size_z for x in xrange(size_y)] for y in xrange(size_x)]
collider_nodes = [[[0]*size_z for x in xrange(size_y)] for y in xrange(size_x)]
# some constants
MARKER_BASE = Vec4(1, 1, 1, 1)
MARKER_HIGH = Vec4(0, 0, 0, 1)
# currently highlighted marker
marker_high = False
def __init__(self):
self.accept('escape', sys.exit) # Escape quits
# base.disableMouse() # Disble mouse camera control
self.init_empty_grid()
self.init_grid()
# Lights
self.init_light()
# Camera
self.init_camera()
self.init_gui()
def init_empty_grid(self):
self.live_grid = [[[0]*(self.size_z+1) for x in xrange(self.size_y+1)] for y in xrange(self.size_x+1)]
def count_neighbours(self, x, y, z):
sum = 0
mod_x = self.size_x + self.wrap_walls
mod_y = self.size_y + self.wrap_walls
mod_z = self.size_z + self.wrap_walls
for dx in range (-1, 2):
for dy in range (-1, 2):
for dz in range (-1, 2):
sum += self.live_grid[(x + dx) % mod_x][(y + dy) % mod_y][(z + dz) % mod_z]
sum -= self.live_grid[x][y][z] # remove the cell itself
return sum
def calculate_grid(self):
new_grid = [[[0]*(self.size_z+1) for x in xrange(self.size_y+1)] for y in xrange(self.size_x+1)]
for x in range (0, self.size_x):
for y in range (0, self.size_y):
for z in range (0, self.size_z):
n = self.count_neighbours(x, y, z)
if(self.live_grid[x][y][z]):
if(n <= self.rule_set["survive_max"] and n >= self.rule_set["survive_min"]): new_grid[x][y][z] = 1
else: new_grid[x][y][z] = 0
else:
if(n <= self.rule_set["new_max"] and n >= self.rule_set["new_min"]): new_grid[x][y][z] = 1
else: new_grid[x][y][z] = 0
if(new_grid[x][y][z]): self.models[x][y][z].show()
else: self.models[x][y][z].hide()
self.live_grid = new_grid
def init_cross(self, x, y, z):
# cross
self.live_grid[x][y-1][z] = 1
self.live_grid[x][y][z] = 1
self.live_grid[x][y+1][z] = 1
self.live_grid[x-1][y][z] = 1
self.live_grid[x+1][y][z] = 1
self.live_grid[x][y][z-1] = 1
self.live_grid[x][y][z+1] = 1
def init_grid(self):
self.init_cross(self.size_x/2, self.size_y/2, self.size_z/2)
#Since we are using collision detection to do picking, we set it up like
#any other collision detection system with a traverser and a handler
self.picker = CollisionTraverser() #Make a traverser
self.pq = CollisionHandlerQueue() #Make a handler
#Make a collision node for our picker ray
self.pickerNode = CollisionNode('mouseRay')
#Attach that node to the camera since the ray will need to be positioned
#relative to it
self.pickerNP = camera.attachNewNode(self.pickerNode)
#Everything to be picked will use bit 1. This way if we were doing other
#collision we could seperate it
self.pickerNode.setFromCollideMask(BitMask32.bit(1))
self.pickerRay = CollisionRay() #Make our ray
self.pickerNode.addSolid(self.pickerRay) #Add it to the collision node
#Register the ray as something that can cause collisions
self.picker.addCollider(self.pickerNP, self.pq)
#self.picker.showCollisions(render)
# create the basic cube (only once)
self.cube = loader.loadModel('models/cube.egg')
self.cube.reparentTo(hidden)
self.edit_marker = loader.loadModel('models/simple_cube.egg')
self.edit_marker.reparentTo(hidden)
self.edit_marker_group = render.attachNewNode("edit_markers")
#self.edit_marker_group.hide()
for x in xrange (self.size_x):
for y in xrange (self.size_y):
for z in xrange (self.size_z):
# built instances of cube and move into the grid
self.models[x][y][z] = render.attachNewNode("cube_" + str(x) + "_" + str(y) + "_" + str(z))
self.cube.instanceTo(self.models[x][y][z])
self.models[x][y][z].setPos(x*2 - self.size_x+1, y*2 - self.size_y+1, z*2 - self.size_z+1)
# add some info
self.models[x][y][z].setTag("cell", str(1))
self.models[x][y][z].setTag("x", str(x))
self.models[x][y][z].setTag("y", str(y))
self.models[x][y][z].setTag("z", str(z))
self.models[x][y][z].setColor(1.0, 0.0, 0.0, 0.0)
if(not self.live_grid[x][y][z]): self.models[x][y][z].hide()
# and the edit_markers
self.edit_markers[x][y][z] = render.attachNewNode("edit_marker_" + str(x) + "_" + str(y) + "_" + str(z))
self.edit_markers[x][y][z].setPos(x*2 - self.size_x+1, y*2 - self.size_y+1, z*2 - self.size_z+1)
self.edit_marker.instanceTo(self.edit_markers[x][y][z])
self.edit_markers[x][y][z].reparentTo(self.edit_marker_group)
self.edit_markers[x][y][z].setColor(1.0, 0.0, 0.0, 0.0)
self.edit_markers[x][y][z].setTag("edit", str(1))
self.edit_markers[x][y][z].setTag("x", str(x))
self.edit_markers[x][y][z].setTag("y", str(y))
self.edit_markers[x][y][z].setTag("z", str(z))
self.collider_spheres[x][y][z] = CollisionSphere(0, 0, 0, .25)
self.collider_nodes[x][y][z] = self.edit_markers[x][y][z].attachNewNode(CollisionNode('cnode'))
self.collider_nodes[x][y][z].node().addSolid(self.collider_spheres[x][y][z])
#self.collider_nodes[x][y][z].show()
self.collider_nodes[x][y][z].setTag("x", str(x))
self.collider_nodes[x][y][z].setTag("y", str(y))
self.collider_nodes[x][y][z].setTag("z", str(z))
#self.edit_markers[x][y][z].node().setIntoCollideMask(BitMask32.bit(1))
#self.edit_markers[x][y][z].find("**/polygon").node().setIntoCollideMask(BitMask32.bit(1))
#Set a tag on the square's node so we can look up what square this is
#self.edit_markers[x][y][z].find("**/polygon").node().setTag('square', str(i))
base.setBackgroundColor(0.35, 0.25, 0.0)
# built sequence to calculate grid
self.calculator = Sequence(Wait(0.75), Func(self.calculate_grid))
#Start the task that handles the picking
self.mouse_task = taskMgr.add(self.mouse_task, 'mouseTask')
self.accept("mouse1", self.click)
# self.accept("mouse1-up", self.releasePiece) #releasing places it
def click(self):
print "click"
def mouse_task(self, task):
# print 'mouse'
# First we check that the mouse is not outside the screen
if base.mouseWatcherNode.hasMouse():
# take screen coordinates and adjust picker ray
mpos = base.mouseWatcherNode.getMouse()
self.pickerRay.setFromLens(base.camNode, mpos.getX(), mpos.getY())
# now traverse collission
self.picker.traverse(render)
# assume for simplicity's sake that myHandler is a CollisionHandlerQueue
if(not self.marker_high is False): self.marker_high.setColor(self.MARKER_BASE)
if self.pq.getNumEntries() > 0:
self.pq.sortEntries() #this is so we get the closest object
cm = self.pq.getEntry(0).getIntoNodePath()
self.marker_high = self.edit_markers[int(cm.getTag('x'))][int(cm.getTag('y'))][int(cm.getTag('z'))]
self.marker_high.setColor(self.MARKER_HIGH)
#self.marker_high.hide()
#print 'pick: {} / {} / {}', (pickedObj.getTag('x'), pickedObj.getTag('y'), pickedObj.getTag('z'))
return Task.cont
def init_light(self):
plight = AmbientLight('my plight')
plight.setColor(VBase4(0.20, 0.20, 0.20, 1))
plnp = render.attachNewNode(plight)
render.setLight(plnp)
light2 = PointLight('pointlight')
plnp2 = render.attachNewNode(light2)
plnp2.setPos(-50, -10, 10)
render.setLight(plnp2)
def init_camera(self):
# Camera
base.camera.setPos(-50, -50, 10)
base.camera.lookAt(self.cube)
mat=Mat4(camera.getMat())
mat.invertInPlace()
base.mouseInterfaceNode.setMat(mat)
def init_gui(self):
self.first_frame = DirectFrame(frameColor=(0,0.5,0,1) , frameSize=(0, 0.75, 0, 0.5 ) , pos =(-1.3, 0, 0.45 ))
self.step_button = DirectButton(pos = Vec3(0.125, 0.0, 0.35), text = ">",
scale = .1, pad = (.5, .5),
rolloverSound = None, clickSound = None,
command = self.calculate_grid)
self.step_button.reparentTo(self.first_frame)
self.start_button = DirectButton(pos = Vec3(0.35, 0.0, 0.35), text = ">>",
scale = .1, pad = (.5, .5),
rolloverSound = None, clickSound = None,
command = self.start_stop)
self.start_button.reparentTo(self.first_frame)
self.button_reset_random = DirectButton(pos = Vec3(0.15, 0.0, 0.1), text = "reset",
scale = .1, pad = (.25, .25),
rolloverSound = None, clickSound = None,
command = self.reset_random)
self.button_reset_random.reparentTo(self.first_frame)
self.slider_rnd = DirectSlider(pos = Vec3(0.5, 0.0, 0.1), text = "0.25", value = 0.25,
scale = .1, pad = (.25, .5),
command = self.set_random_base)
self.slider_rnd.reparentTo(self.first_frame)
self.button_reset_cross = DirectButton(pos = Vec3(0.15, 0.0, -0.1), text = "reset +",
scale = .1, pad = (.25, .25),
rolloverSound = None, clickSound = None,
command = self.reset_cross)
self.button_reset_cross.reparentTo(self.first_frame)
# rulesetter
self.button_new_min_minus = DirectButton(pos = Vec3(-1.1, 0.0, 0.0), text = "<",
scale = .075, pad = (.3, .2), rolloverSound = None, clickSound = None,
command = self.set_rule, extraArgs = ("new_min", -1))
self.button_new_min_plus = DirectButton(pos = Vec3(-0.9, 0.0, 0.0), text = ">",
scale = .075, pad = (.3, .2), rolloverSound = None, clickSound = None,
command = self.set_rule, extraArgs = ("new_min", 1))
self.button_new_min_minus = DirectButton(pos = Vec3(-1.1, 0.0, -0.1), text = "<",
scale = .075, pad = (.3, .2), rolloverSound = None, clickSound = None,
command = self.set_rule, extraArgs = ("new_max", -1))
self.button_new_min_plus = DirectButton(pos = Vec3(-0.9, 0.0, -0.1), text = ">",
scale = .075, pad = (.3, .2), rolloverSound = None, clickSound = None,
command = self.set_rule, extraArgs = ("new_max", 1))
self.button_new_min_minus = DirectButton(pos = Vec3(-1.1, 0.0, -0.2), text = "<",
scale = .075, pad = (.3, .2), rolloverSound = None, clickSound = None,
command = self.set_rule, extraArgs = ("survive_min", -1))
self.button_new_min_plus = DirectButton(pos = Vec3(-0.9, 0.0, -0.2), text = ">",
scale = .075, pad = (.3, .2), rolloverSound = None, clickSound = None,
command = self.set_rule, extraArgs = ("survive_min", 1))
self.button_new_min_minus = DirectButton(pos = Vec3(-1.1, 0.0, -0.3), text = "<",
scale = .075, pad = (.3, .2), rolloverSound = None, clickSound = None,
command = self.set_rule, extraArgs = ("survive_max", -1))
self.button_new_min_plus = DirectButton(pos = Vec3(-0.9, 0.0, -0.3), text = ">",
scale = .075, pad = (.3, .2), rolloverSound = None, clickSound = None,
command = self.set_rule, extraArgs = ("survive_max", 1))
self.rule_set_labels = {
"new_min" :
DirectLabel(pos = Vec3(-1.0, 0.0, 0.0), text = str(self.rule_set["new_min"]),
scale = .075, pad = (.5, .2)),
"new_max" :
DirectLabel(pos = Vec3(-1.0, 0.0, -0.1), text = str(self.rule_set["new_max"]),
scale = .075, pad = (.5, .2)),
"survive_min" :
DirectLabel(pos = Vec3(-1.0, 0.0, -0.2), text = str(self.rule_set["survive_min"]),
scale = .075, pad = (.5, .2)),
"survive_max" :
DirectLabel(pos = Vec3(-1.0, 0.0, -0.3), text = str(self.rule_set["survive_max"]),
scale = .075, pad = (.5, .2))
}
self.button_check_wrap = DirectCheckButton(pos = Vec3(-1.0, 0.0, 0.65), text = "wrap walls",
scale = .075, pad = (.3, .2), rolloverSound = None, clickSound = None,
command = self.set_wrap, indicatorValue = not self.wrap_walls)
self.button_check_wrap.reparentTo(self.first_frame)
def set_rule(self, what, val = 0):
if(self.rule_set[what] + val >= 0 and self.rule_set[what] + val <= 26): self.rule_set[what] += val
self.rule_set_labels[what]["text"] = str(self.rule_set[what])
def set_wrap(self, status):
self.wrap_walls = not status
def reset_cross(self):
self.init_empty_grid()
self.init_cross(self.size_x/2, self.size_y/2, self.size_z/2)
for x in xrange (self.size_x):
for y in xrange (self.size_y):
for z in xrange (self.size_z):
if(not self.live_grid[x][y][z]): self.models[x][y][z].hide()
else: self.models[x][y][z].show()
def reset_random(self):
import random
self.init_empty_grid()
for x in xrange (self.size_x):
for y in xrange (self.size_y):
for z in xrange (self.size_z):
self.live_grid[x][y][z] = (random.random() < self.slider_rnd["value"])
if(not self.live_grid[x][y][z]): self.models[x][y][z].hide()
else: self.models[x][y][z].show()
def set_random_base(self):
self.slider_rnd["text"] = '{0:.3f}.'.format(self.slider_rnd["value"])
def start_stop(self):
if(self.calculator.isStopped()): self.calculator.loop()
else: self.calculator.finish()
# Application code
if __name__ == "__main__":
w = World()
run()