DirectJoybox.py 19 KB

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