class bullet(): #tuple pos, tuple target, float damage, float speed, float range, float accuracy -> bullet def __init__(self, sender, pos, dir, damage, speed, range, accuracy=1): #bullets have a speed, a model, and a damage. Both should be parameters #range as well self.start = pos self.sender = sender self.accuracy = accuracy self.direction = dir.normalized() self.damage = damage self.tasks = [] self.speed = speed self.range = range self.model = base.loader.loadModel('models/bullet') self.model.reparentTo(base.render) self.model.setPos(pos) #self.target=target #create collision sphere #TODO account for larger bullet sizes cs = CollisionSphere(0, 0, 0, 1) self.mainCol = CollisionNode('cNode') #set up circular reference so collision volume knows parent self.mainCol.setPythonTag('owner', self) self.mainCol.setIntoCollideMask( CollideMask.bit(1) ) # send objects with intoCollideMask bit 1. Relates to most entities self.cNode = self.model.attachNewNode(self.mainCol) self.cNode.node().addSolid(cs) #create collider handler, if one doesn't already exist try: # attempt to add to global collision traverser base.cTrav.addCollider(self.cNode, base.bulletCollider) except AttributeError: base.bulletCollider = CollisionHandlerEvent() # create event called 'bulletCollision' on hit base.bulletCollider.addInPattern('bulletCollision') finally: # retry base.cTrav.addCollider(self.cNode, base.bulletCollider) #rotate model such that it follows the passed direction argument vector self.model.setHpr(helper.vectorToHPR(dir)) #assign direction based on rotation #THIS TOOK WAY TOO MUCH EFFORT TO FIGURE OUT #normVec here is the model nodepath taking the passed argument vector (dir) #belonging to another node (base.render) #and adjusting it to its local coordinate space - what would this vector look like from #self.model's perspective. Then it is normalized and assigned. normVec = self.model.getRelativeVector(base.render, dir) normVec.normalize() self.direction = normVec #normVec=base.render.getRelativeVector(self.model,Vec3(0,1,0)) #print(self.model.getHpr()) #print(normVec) #start task to move forward at speed self.tasks.append(taskMgr.add(self.accelerate, "bulletAccelerateTask")) #task: accelerate def accelerate(self, task): """ Task moves bullet forward until it hits an object or range is met """ #range is decremented each tick #check to make sure not 0 if self.range <= 0: #if range has ran out kill task and this object self.delete() return task.done #otherwise proceed, move object and decrement range dt = globalClock.getDt() #distVec=min((self.start-self.target),(self.target-self.start)) #distVec=distVec.normalized() #print(distVec) #print(self.direction) #take normalized direction vector and apply to transform self.model.setFluidX(self.model, self.direction[0] * self.speed * dt) self.model.setFluidY(self.model, self.direction[1] * self.speed * dt) self.model.setFluidZ(self.model, self.direction[2] * self.speed * dt) self.range -= (self.speed * dt) return task.cont #class deconstructor def delete(self): self.model.hide() #clear tasks for task in self.tasks: taskMgr.remove(task) #clear collision self.mainCol.clearPythonTag("owner") #remove object self.model.removeNode()
class entity(Actor): ''' Base class for any characters Contains methods for movement, collision, model loading ''' #procedure class constructor # NodePath model, ShowBase caller, tuple Pos -> class construction def __init__(self, model: str, base: ShowBase, pos: tuple, physics=True): ''' constructor for entity. attaches model to calling instance renderer also stores size definitions and creates basic collision object ''' Actor.__init__(self) DEFAULT_HEALTH = 50 #reference base for later self.base = base # set health self.health = DEFAULT_HEALTH self.damagedSound = base.loader.loadSfx("sounds/oof.ogg") # speed self.speed = 10 self.turnSpeed = 5 # gravity -- is touching ground? self.isGrounded = False # creates actor object using constructor and parents to passed renderer self.loadModel(model) self.renderer = base.render self.reparentTo(self.renderer) # put at specified location self.setPos(pos) # store dimensions for later # https://discourse.panda3d.org/t/getting-the-height-width-and-length-of-models-solved/6504 # Post describes the output of the Nodepath class's getTightBounds() method. Using this # I am able to get an approximation of the dimensions of an object in the global coordinate space minimum, maximum = self.getTightBounds() # make sure all numbers are positive for best (any) results self.bounds = [abs(num) for num in (minimum - maximum)] self.width, self.length, self.height = self.bounds[0], self.bounds[ 1], self.bounds[2] # COLLISION PROPERTIES # create collision ray that is height of model pointing down (will detect ground collisions) if physics: self.groundRay = CollisionRay() self.groundRay.setOrigin(0, 0, 1000) self.groundRay.setDirection(0, 0, -1) self.groundCol = CollisionNode('groundRay') self.groundCol.addSolid(self.groundRay) self.groundCol.setFromCollideMask(BitMask32.bit(0)) self.groundCol.setIntoCollideMask(BitMask32.allOff()) self.groundColNode = self.attachNewNode(self.groundCol) base.cTrav.addCollider(self.groundColNode, base.groundHandler) #and another one for everything else self.mainCol = CollisionNode('actorCollision' + str(id(self))) # create collision sphere as solid for this collision node self.mainCol.addSolid( CollisionSphere(0, 0, self.height / 2, self.height / 2)) # specify valid collisions for collision node self.mainCol.setFromCollideMask(CollideMask.bit(0)) self.mainCol.setIntoCollideMask(CollideMask.bit( 1)) # accepts incoming objects with collideMask bit(1) # attach collision node to actor self.cNode = self.attachNewNode(self.mainCol) # show #self.cNode.show() # make instance collision traverser aware of this collision node, tell it how to handle (with pusher) base.cTrav.addCollider(self.cNode, base.pusher) # add collision to pusher collision handler; tell pusher which node to associate with which actor IOT push base.pusher.addCollider(self.cNode, self, base.drive.node()) # add to base.entities (since all allies/enemies created through this constructor, makes sense base.entities.append(self) # add as client of entity collision handler # base.cTrav.addCollider(self.cNode,base.entityCollisionHandler) #add to cleanup for deletion later base.cleanup.append(self) #store reference to self #https://discourse.panda3d.org/t/inheriting-from-nodepath/10886/4 #post describes how to set up a reference from nodepath to itself to retrieve custom properties #I use python tags to distinguish collision volume owners self.mainCol.setPythonTag("owner", self) # TODO if no initial collision, enforce gravity until collision #taskMgr.add(self.doGravity, "entityGravity") #procedure update #do all actions that must be done for this object every frame - called from game loop def updateState(self): #by default do gravity for all entities self.doGravity() return #procedure entityGravity # drops entity by gravity if isGrounded is not true (set True by collision with ground) def doGravity(self): dt = globalClock.getDt() #gravity - if not grounded, make it so if not self.isGrounded: self.setZ(self.getZ() - 50 * dt) return else: entries = [] for entry in base.groundHandler.getEntries(): if entry.getFromNodePath().getParent() == self \ or entry.getIntoNodePath().getParent() == self: entries.append(entry) if (len(entries) > 0) and (entries[0].getIntoNode().getName() == "terrain"): self.setZ(entries[0].getSurfacePoint(base.render).getZ() + 2) #procedure add Damage #float dmg -> decrement self.health def addDamage(self, sender, dmg): ''' Adds a specified amount to this entity Can be negative or positive If self.health < 0 call delete self ''' #get shooter for scoring purposes shooter = sender.owner # TODO make pretty death self.health -= dmg if self.damagedSound is not None: self.damagedSound.play() if self.health <= 0: #if shooter was player, add to score if shooter is base.player: if shooter.team is self.team: base.player.score -= 15 else: base.player.score += 15 #if player, end game if self is base.player: messenger.send('Leave-Game') self.kill() #procedure kill # destroys object, but fancy-like def kill(self): self.pose('death', 0) taskMgr.add(self.deleteTask, 'deleteTask') #procedure deleteTask #destroys object after five seconds of being in death state def deleteTask(self, task): if task.time < 5.0: return task.cont else: self.delete() return task.done #procedure class deconstructor def delete(self): ''' Class destructor Destroy an object cleanly from instance ''' #also remove from base.entities if self in base.entities: base.entities.remove(self) # remove pythontag from collision self.mainCol.clearPythonTag("owner") # remove collision node from global collider base.cTrav.removeCollider(self.cNode) #destroy actor if not self.is_empty(): self.hide() self.cleanup() #self.detachNode() del self #class deconstructor def __del__(self): self.removeNode()