DirectJoybox.py 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515
  1. """ Class used to create and control joybox device """
  2. from direct.showbase.DirectObject import DirectObject
  3. from DirectDeviceManager import *
  4. from direct.directtools.DirectUtil import *
  5. from direct.gui import OnscreenText
  6. from direct.task import Task
  7. import math
  8. """
  9. TODO:
  10. Handle interaction between widget, followSelectedTask and updateTask
  11. """
  12. # BUTTONS
  13. L_STICK = 0
  14. L_UPPER = 1
  15. L_LOWER = 2
  16. R_STICK = 3
  17. R_UPPER = 4
  18. R_LOWER = 5
  19. # ANALOGS
  20. NULL_AXIS = -1
  21. L_LEFT_RIGHT = 0
  22. L_FWD_BACK = 1
  23. L_TWIST = 2
  24. L_SLIDE = 3
  25. R_LEFT_RIGHT = 4
  26. R_FWD_BACK = 5
  27. R_TWIST = 6
  28. R_SLIDE = 7
  29. JOYBOX_MIN = ANALOG_MIN + ANALOG_DEADBAND
  30. JOYBOX_MAX = ANALOG_MAX - ANALOG_DEADBAND
  31. JOYBOX_RANGE = JOYBOX_MAX - JOYBOX_MIN
  32. JOYBOX_TREAD_SEPERATION = 1.0
  33. class DirectJoybox(DirectObject):
  34. joyboxCount = 0
  35. xyzMultiplier = 1.0
  36. hprMultiplier = 1.0
  37. def __init__(self, device = 'CerealBox', nodePath = base.direct.camera,
  38. headingNP = base.direct.camera):
  39. # See if device manager has been initialized
  40. if base.direct.deviceManager == None:
  41. base.direct.deviceManager = DirectDeviceManager()
  42. # Set name
  43. DirectJoybox.joyboxCount += 1
  44. self.name = 'Joybox-' + repr(DirectJoybox.joyboxCount)
  45. # Get buttons and analogs
  46. self.device = device
  47. self.analogs = base.direct.deviceManager.createAnalogs(self.device)
  48. self.buttons = base.direct.deviceManager.createButtons(self.device)
  49. self.aList = [0, 0, 0, 0, 0, 0, 0, 0]
  50. self.bList = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
  51. # For joybox fly mode
  52. # Default is joe mode
  53. self.mapping = [R_LEFT_RIGHT, R_FWD_BACK, L_FWD_BACK,
  54. R_TWIST, L_TWIST, NULL_AXIS]
  55. self.modifier = [1, 1, 1, -1, -1, 0]
  56. # Initialize time
  57. self.lastTime = globalClock.getFrameTime()
  58. # Record node path
  59. self.nodePath = nodePath
  60. self.headingNP = headingNP
  61. self.useHeadingNP = False
  62. self.rotateInPlace = False
  63. self.floatingNP = NodePath("floating")
  64. # Ref CS for orbit mode
  65. self.refCS = base.direct.cameraControl.coaMarker
  66. self.tempCS = base.direct.group.attachNewNode('JoyboxTempCS')
  67. # Text object to display current mode
  68. self.readout = OnscreenText.OnscreenText(
  69. pos = (-0.9, 0.95),
  70. font = base.direct.font,
  71. mayChange = 1)
  72. # List of functions to cycle through
  73. self.modeList = [self.joeMode, self.driveMode, self.orbitMode]
  74. # Pick initial mode
  75. self.updateFunc = self.joyboxFly
  76. self.modeName = 'Joe Mode'
  77. # Auxiliary data
  78. self.auxData = []
  79. # Button registry
  80. self.addButtonEvents()
  81. # Spawn update task
  82. self.enable()
  83. def setHeadingNodePath(self,np):
  84. self.headingNP = np
  85. def enable(self):
  86. # Kill existing task
  87. self.disable()
  88. # Accept button events
  89. self.acceptSwitchModeEvent()
  90. self.acceptUprightCameraEvent()
  91. # Update task
  92. taskMgr.add(self.updateTask, self.name + '-updateTask')
  93. def disable(self):
  94. taskMgr.remove(self.name + '-updateTask')
  95. # Ignore button events
  96. self.ignoreSwitchModeEvent()
  97. self.ignoreUprightCameraEvent()
  98. def destroy(self):
  99. self.disable()
  100. self.tempCS.removeNode()
  101. def addButtonEvents(self):
  102. self.breg = ButtonRegistry.ptr()
  103. # MRM: Hard coded!
  104. for i in range(8):
  105. self.buttons.setButtonMap(
  106. i, self.breg.getButton(self.getEventName(i)))
  107. self.eventThrower = self.buttons.getNodePath().attachNewNode(
  108. ButtonThrower('JB Button Thrower'))
  109. def setNodePath(self, nodePath):
  110. self.nodePath = nodePath
  111. def getNodePath(self):
  112. return self.nodePath
  113. def setRefCS(self, refCS):
  114. self.refCS = refCS
  115. def getRefCS(self):
  116. return self.refCS
  117. def getEventName(self, index):
  118. return self.name + '-button-' + repr(index)
  119. def setXyzMultiplier(self, multiplier):
  120. DirectJoybox.xyzMultiplier = multiplier
  121. def getXyzMultiplier(self):
  122. return DirectJoybox.xyzMultiplier
  123. def setHprMultiplier(self, multiplier):
  124. DirectJoybox.hprMultiplier = multiplier
  125. def getHprMultiplier(self):
  126. return DirectJoybox.hprMultiplier
  127. def updateTask(self, state):
  128. # old optimization
  129. #self.updateValsUnrolled()
  130. self.updateVals()
  131. self.updateFunc()
  132. return Task.cont
  133. def updateVals(self):
  134. # Update delta time
  135. cTime = globalClock.getFrameTime()
  136. self.deltaTime = cTime - self.lastTime
  137. self.lastTime = cTime
  138. # Update analogs
  139. for i in range(len(self.analogs)):
  140. self.aList[i] = self.normalizeChannel(i)
  141. # Update buttons
  142. for i in range(len(self.buttons)):
  143. try:
  144. self.bList[i] = self.buttons[i]
  145. except IndexError:
  146. # That channel may not have been updated yet
  147. self.bList[i] = 0
  148. def updateValsUnrolled(self):
  149. # Update delta time
  150. cTime = globalClock.getFrameTime()
  151. self.deltaTime = cTime - self.lastTime
  152. self.lastTime = cTime
  153. # Update analogs
  154. for chan in range(len(self.analogs)):
  155. val = self.analogs.getControlState(chan)
  156. # Zero out values in deadband
  157. if (val < 0):
  158. val = min(val + ANALOG_DEADBAND, 0.0)
  159. else:
  160. val = max(val - ANALOG_DEADBAND, 0.0)
  161. # Scale up rotating knob values
  162. if (chan == L_TWIST) or (chan == R_TWIST):
  163. val *= 3.0
  164. # Now clamp value between minVal and maxVal
  165. val = CLAMP(val, JOYBOX_MIN, JOYBOX_MAX)
  166. self.aList[chan] = 2.0*((val - JOYBOX_MIN)/JOYBOX_RANGE) - 1
  167. # Update buttons
  168. for i in range(len(self.buttons)):
  169. try:
  170. self.bList[i] = self.buttons.getButtonState(i)
  171. except IndexError:
  172. # That channel may not have been updated yet
  173. self.bList[i] = 0
  174. def acceptSwitchModeEvent(self, button = R_UPPER):
  175. self.accept(self.getEventName(button), self.switchMode)
  176. def ignoreSwitchModeEvent(self, button = R_UPPER):
  177. self.ignore(self.getEventName(button))
  178. def switchMode(self):
  179. try:
  180. # Get current mode
  181. self.modeFunc = self.modeList[0]
  182. # Rotate mode list
  183. self.modeList = self.modeList[1:] + self.modeList[:1]
  184. # Call new mode
  185. self.modeFunc()
  186. except IndexError:
  187. pass
  188. def showMode(self, modeText):
  189. def hideText(state, s = self):
  190. s.readout.setText('')
  191. return Task.done
  192. taskMgr.remove(self.name + '-showMode')
  193. # Update display
  194. self.readout.setText(modeText)
  195. t = taskMgr.doMethodLater(3.0, hideText, self.name + '-showMode')
  196. t.setUponDeath(hideText)
  197. def acceptUprightCameraEvent(self, button = L_UPPER):
  198. self.accept(self.getEventName(button),
  199. base.direct.cameraControl.orbitUprightCam)
  200. def ignoreUprightCameraEvent(self, button = L_UPPER):
  201. self.ignore(self.getEventName(button))
  202. def setMode(self, func, name):
  203. self.disable()
  204. self.updateFunc = func
  205. self.modeName = name
  206. self.showMode(self.modeName)
  207. self.enable()
  208. def setUseHeadingNP(self,enabled):
  209. self.useHeadingNP = enabled
  210. def setRotateInPlace(self,enabled):
  211. self.rotateInPlace = enabled
  212. def joyboxFly(self):
  213. # Do nothing if no nodePath selected
  214. if self.nodePath == None:
  215. return
  216. hprScale = ((self.aList[L_SLIDE] + 1.0) *
  217. 50.0 * DirectJoybox.hprMultiplier)
  218. posScale = ((self.aList[R_SLIDE] + 1.0) *
  219. 50.0 * DirectJoybox.xyzMultiplier)
  220. def getAxisVal(index, s = self):
  221. try:
  222. return s.aList[s.mapping[index]]
  223. except IndexError:
  224. # If it is a null axis return 0
  225. return 0.0
  226. x = getAxisVal(0) * self.modifier[0]
  227. y = getAxisVal(1) * self.modifier[1]
  228. z = getAxisVal(2) * self.modifier[2]
  229. pos = Vec3(x, y, z) * (posScale * self.deltaTime)
  230. h = getAxisVal(3) * self.modifier[3]
  231. p = getAxisVal(4) * self.modifier[4]
  232. r = getAxisVal(5) * self.modifier[5]
  233. hpr = Vec3(h, p, r) * (hprScale * self.deltaTime)
  234. # if we are using a heading nodepath, we want
  235. # to drive in the direction we are facing,
  236. # however, we don't want the z component to change
  237. if (self.useHeadingNP and self.headingNP != None):
  238. oldZ = pos.getZ()
  239. pos = self.nodePath.getRelativeVector(self.headingNP,
  240. pos)
  241. pos.setZ(oldZ)
  242. # if we are using a heading NP we might want to rotate
  243. # in place around that NP
  244. if (self.rotateInPlace):
  245. parent = self.nodePath.getParent()
  246. self.floatingNP.reparentTo(parent)
  247. self.floatingNP.setPos(self.headingNP,0,0,0)
  248. self.floatingNP.setHpr(0,0,0)
  249. self.nodePath.wrtReparentTo(self.floatingNP)
  250. self.floatingNP.setHpr(hpr)
  251. self.nodePath.wrtReparentTo(parent)
  252. hpr = Vec3(0,0,0)
  253. self.nodePath.setPosHpr(self.nodePath, pos, hpr)
  254. def joeMode(self):
  255. self.mapping = [R_LEFT_RIGHT, R_FWD_BACK, L_FWD_BACK,
  256. R_TWIST, L_TWIST, NULL_AXIS]
  257. self.modifier = [1, 1, 1, -1, -1, 0]
  258. self.setMode(self.joyboxFly, 'Joe Mode')
  259. def basicMode(self):
  260. self.mapping = [NULL_AXIS, R_FWD_BACK, NULL_AXIS,
  261. R_LEFT_RIGHT, NULL_AXIS, NULL_AXIS]
  262. self.modifier = [0,1,0,-1,0,0]
  263. self.setMode(self.joyboxFly,'Basic Mode')
  264. def fpsMode(self):
  265. self.mapping = [L_LEFT_RIGHT,R_FWD_BACK,L_FWD_BACK,
  266. R_LEFT_RIGHT, NULL_AXIS, NULL_AXIS]
  267. self.modifier = [1,1,1,-1,0,0]
  268. self.setMode(self.joyboxFly,'FPS Mode')
  269. def tankMode(self):
  270. self.setMode(self.tankFly,'Tank Mode')
  271. def nullMode(self):
  272. self.setMode(self.nullFly,'Null Mode')
  273. def lucMode(self):
  274. self.mapping = [R_LEFT_RIGHT, R_FWD_BACK, L_FWD_BACK,
  275. R_TWIST, L_TWIST, L_LEFT_RIGHT]
  276. self.modifier = [1, 1, 1, -1, -1, 0]
  277. self.setMode(self.joyboxFly, 'Luc Mode')
  278. def driveMode(self):
  279. self.mapping = [L_LEFT_RIGHT, R_FWD_BACK, R_TWIST,
  280. R_LEFT_RIGHT, L_FWD_BACK, NULL_AXIS]
  281. self.modifier = [1, 1, -1, -1, -1, 0]
  282. self.setMode(self.joyboxFly, 'Drive Mode')
  283. def lookAtMode(self):
  284. self.mapping = [R_LEFT_RIGHT, R_TWIST, R_FWD_BACK,
  285. L_LEFT_RIGHT, L_FWD_BACK, NULL_AXIS]
  286. self.modifier = [1, 1, 1, -1, 1, 0]
  287. self.setMode(self.joyboxFly, 'Look At Mode')
  288. def lookAroundMode(self):
  289. self.mapping = [NULL_AXIS, NULL_AXIS, NULL_AXIS,
  290. R_LEFT_RIGHT, R_FWD_BACK, NULL_AXIS]
  291. self.modifier = [0, 0, 0, -1, -1, 0]
  292. self.setMode(self.joyboxFly, 'Lookaround Mode')
  293. def demoMode(self):
  294. self.mapping = [R_LEFT_RIGHT, R_FWD_BACK, L_FWD_BACK,
  295. R_TWIST, NULL_AXIS, NULL_AXIS]
  296. self.modifier = [1, 1, 1, -1, 0, 0]
  297. self.setMode(self.joyboxFly, 'Demo Mode')
  298. def hprXyzMode(self):
  299. self.mapping = [R_LEFT_RIGHT, R_FWD_BACK, R_TWIST,
  300. L_TWIST, L_FWD_BACK, L_LEFT_RIGHT]
  301. self.modifier = [1, 1, -1, -1, -1, 1]
  302. self.setMode(self.joyboxFly, 'HprXyz Mode')
  303. def mopathMode(self):
  304. self.mapping = [R_LEFT_RIGHT, R_FWD_BACK, R_TWIST,
  305. L_LEFT_RIGHT, L_FWD_BACK, L_LEFT_RIGHT]
  306. self.modifier = [1, 1, -1, -1, 1, 0]
  307. self.setMode(self.joyboxFly, 'Mopath Mode')
  308. def walkthruMode(self):
  309. self.mapping = [R_LEFT_RIGHT, R_FWD_BACK, L_TWIST,
  310. R_TWIST, L_FWD_BACK, L_LEFT_RIGHT]
  311. self.modifier = [1, 1, -1, -1, -1, 1]
  312. self.setMode(self.joyboxFly, 'Walkthru Mode')
  313. def spaceMode(self):
  314. self.setMode(self.spaceFly, 'Space Mode')
  315. def nullFly(self):
  316. return
  317. def tankFly(self):
  318. leftTreadSpeed = (self.normalizeChannel(L_SLIDE,.1,100) *
  319. DirectJoybox.xyzMultiplier) * self.aList[L_FWD_BACK]
  320. rightTreadSpeed = (self.normalizeChannel(R_SLIDE,.1,100) *
  321. DirectJoybox.xyzMultiplier) * self.aList[R_FWD_BACK]
  322. forwardSpeed = (leftTreadSpeed + rightTreadSpeed)*.5
  323. headingSpeed = math.atan2(leftTreadSpeed - rightTreadSpeed,
  324. JOYBOX_TREAD_SEPERATION)
  325. headingSpeed = 180/3.14159 * headingSpeed
  326. dh = -1.0*headingSpeed * self.deltaTime*.3
  327. dy = forwardSpeed * self.deltaTime
  328. self.nodePath.setH(self.nodePath,dh)
  329. self.nodePath.setY(self.nodePath,dy)
  330. def spaceFly(self):
  331. # Do nothing if no nodePath selected
  332. if self.nodePath == None:
  333. return
  334. hprScale = (self.normalizeChannel(L_SLIDE, 0.1, 100) *
  335. DirectJoybox.hprMultiplier)
  336. posScale = (self.normalizeChannel(R_SLIDE, 0.1, 100) *
  337. DirectJoybox.xyzMultiplier)
  338. dr = -1 * hprScale * self.aList[R_TWIST] * self.deltaTime
  339. dp = -1 * hprScale * self.aList[R_FWD_BACK] * self.deltaTime
  340. dh = -1 * hprScale * self.aList[R_LEFT_RIGHT] * self.deltaTime
  341. self.nodePath.setHpr(self.nodePath, dh, dp, dr)
  342. dy = posScale * self.aList[L_FWD_BACK] * self.deltaTime
  343. self.nodePath.setY(self.nodePath, dy)
  344. def planetMode(self, auxData = []):
  345. self.auxData = auxData
  346. self.setMode(self.planetFly, 'Space Mode')
  347. def planetFly(self):
  348. # Do nothing if no nodePath selected
  349. if self.nodePath == None:
  350. return
  351. hprScale = (self.normalizeChannel(L_SLIDE, 0.1, 100) *
  352. DirectJoybox.hprMultiplier)
  353. posScale = (self.normalizeChannel(R_SLIDE, 0.1, 100) *
  354. DirectJoybox.xyzMultiplier)
  355. dr = -1 * hprScale * self.aList[R_TWIST] * self.deltaTime
  356. dp = -1 * hprScale * self.aList[R_FWD_BACK] * self.deltaTime
  357. dh = -1 * hprScale * self.aList[R_LEFT_RIGHT] * self.deltaTime
  358. self.nodePath.setHpr(self.nodePath, dh, dp, dr)
  359. dy = posScale * self.aList[L_FWD_BACK] * self.deltaTime
  360. dPos = VBase3(0, dy, 0)
  361. for planet, radius in self.auxData:
  362. # Are we within min radius?
  363. # How far above planet are we?
  364. np2planet = Vec3(self.nodePath.getPos(planet))
  365. # Compute dist
  366. offsetDist = np2planet.length()
  367. # Above threshold, leave velocity vec as is
  368. if offsetDist > (1.2 * radius):
  369. pass
  370. else:
  371. # Getting close, slow things down
  372. # Compute normal vector through node Path
  373. oNorm = Vec3()
  374. oNorm.assign(np2planet)
  375. oNorm.normalize()
  376. # Xform fly vec to planet space
  377. dPlanet = self.nodePath.getMat(planet).xformVec(Vec3(0, dy, 0))
  378. # Compute radial component of fly vec
  379. dotProd = oNorm.dot(dPlanet)
  380. if dotProd < 0:
  381. # Trying to fly below radius, compute radial component
  382. radialComponent = oNorm * dotProd
  383. # How far above?
  384. above = offsetDist - radius
  385. # Set sf accordingly
  386. sf = max(1.0 - (max(above, 0.0)/(0.2 * radius)), 0.0)
  387. # Subtract scaled radial component
  388. dPlanet -= radialComponent * (sf * sf)
  389. #dPlanet -= radialComponent
  390. # Convert back to node path space
  391. dPos.assign(planet.getMat(self.nodePath).xformVec(dPlanet))
  392. # Set pos accordingly
  393. self.nodePath.setPos(self.nodePath, dPos)
  394. def orbitMode(self):
  395. self.setMode(self.orbitFly, 'Orbit Mode')
  396. def orbitFly(self):
  397. # Do nothing if no nodePath selected
  398. if self.nodePath == None:
  399. return
  400. hprScale = (self.normalizeChannel(L_SLIDE, 0.1, 100) *
  401. DirectJoybox.hprMultiplier)
  402. posScale = (self.normalizeChannel(R_SLIDE, 0.1, 100) *
  403. DirectJoybox.xyzMultiplier)
  404. r = -0.01 * posScale * self.aList[R_TWIST] * self.deltaTime
  405. rx = hprScale * self.aList[R_LEFT_RIGHT] * self.deltaTime
  406. ry = -hprScale * self.aList[R_FWD_BACK] * self.deltaTime
  407. x = posScale * self.aList[L_LEFT_RIGHT] * self.deltaTime
  408. z = posScale * self.aList[L_FWD_BACK] * self.deltaTime
  409. h = -1 * hprScale * self.aList[L_TWIST] * self.deltaTime
  410. # Move dcs
  411. self.nodePath.setX(self.nodePath, x)
  412. self.nodePath.setZ(self.nodePath, z)
  413. self.nodePath.setH(self.nodePath, h)
  414. self.orbitNode(rx, ry, 0)
  415. pos = self.nodePath.getPos(self.refCS)
  416. if Vec3(pos).length() < 0.005:
  417. pos.set(0, -0.01, 0)
  418. # Now move on out
  419. pos.assign(pos * (1 + r))
  420. self.nodePath.setPos(self.refCS, pos)
  421. def orbitNode(self, h, p, r):
  422. # Position the temp node path at the ref CS
  423. self.tempCS.setPos(self.refCS, 0, 0, 0)
  424. # Orient the temp node path to align with the orbiting node path
  425. self.tempCS.setHpr(self.nodePath, 0, 0, 0)
  426. # Record the position of the orbiter wrt the helper
  427. pos = self.nodePath.getPos(self.tempCS)
  428. # Turn the temp node path
  429. self.tempCS.setHpr(self.tempCS, h, p, r)
  430. # Position the orbiter "back" to its position wrt the helper
  431. self.nodePath.setPos(self.tempCS, pos)
  432. # Restore the original hpr of the orbiter
  433. self.nodePath.setHpr(self.tempCS, 0, 0, 0)
  434. # We need to override the DirectAnalog normalizeChannel to
  435. # correct the ranges of the two twist axes of the joybox.
  436. def normalizeChannel(self, chan, minVal = -1, maxVal = 1):
  437. try:
  438. if (chan == L_TWIST) or (chan == R_TWIST):
  439. # These channels have reduced range
  440. return self.analogs.normalize(
  441. self.analogs.getControlState(chan), minVal, maxVal, 3.0)
  442. else:
  443. return self.analogs.normalize(
  444. self.analogs.getControlState(chan), minVal, maxVal)
  445. except IndexError:
  446. return 0.0